Compare commits

...
Author SHA1 Message Date
nala 98fc16a3fb docs: record installed stabilization reference and finite remaining scope 2026-10-24 11:10:51 +02:00
nala 276d833a4b test: consolidate stabilization reference and preserve accepted contour scope
tests / ci (push) Canceled after 0s
2026-10-24 11:08:35 +02:00
nala 5d45541a11 docs: receipt for installed shared map stabilization and accepted public contours 2026-10-24 10:51:31 +02:00
nala 066707ece6 fix: restore shared map access and publish accepted administrative boundary projection
tests / ci (push) Canceled after 0s
2026-10-24 10:47:13 +02:00
nala c1f4a71963 docs: record installed anchor integration and exact territorial recovery 2026-10-23 19:26:07 +02:00
nala cf2ddf21a1 fix: verify territorial recovery manifest with canonical Git line endings
tests / ci (push) Canceled after 0s
2026-10-23 19:20:25 +02:00
nala 3fd5128c62 fix: connect documentary anchors to investment preparation and recover accepted territories
tests / ci (push) Canceled after 0s
2026-10-23 19:17:15 +02:00
nala cdf8fcfba4 docs: record installed treatment scope and verified native PDF 2026-10-23 18:50:13 +02:00
nala 8036b9b071 docs: verify saved native PDF and release delivery condition
tests / ci (push) Canceled after 0s
2026-10-23 18:44:09 +02:00
nala 6985f82e9c docs: delimit anchor findings and pending native PDF delivery gate 2026-10-23 18:36:08 +02:00
nala 2856240cf4 fix: print selected requirements and translated opportunity stage states 2026-10-23 18:34:39 +02:00
nala 32144aa2f3 docs: record treatment scope QA and anchor search analysis boundary 2026-10-23 18:26:00 +02:00
nala 1afdfee8c0 fix: share selected opportunity across simulator stages and land shortcuts 2026-10-23 18:25:09 +02:00
nala bc84fbdb3e docs: delimit remaining transversal investment scope 2026-10-23 18:06:42 +02:00
nala b6a626c92d Record installed opportunity-consumer delta in homologation 2026-10-23 17:58:15 +02:00
nala 55136b446d Record directed QA proof for all four opportunity consumers
tests / ci (push) Canceled after 0s
2026-10-23 17:51:45 +02:00
nala 4a01e9e2b7 Scope parks and downstream investment requests to the treated opportunity 2026-10-23 17:49:22 +02:00
nala ad4160f040 Record second-opportunity context assessment and bounded consumer gaps 2026-10-23 17:18:55 +02:00
nala af9261809c Record installed complements delta and preserved acceptance 2026-10-23 17:14:04 +02:00
nala d8224ba84b Align public simulator entry check with integrated home layout
tests / ci (push) Canceled after 0s
2026-10-23 17:11:15 +02:00
nala 088f2c2cf4 Merge remote-tracking branch 'origin/main' into codex/investment-complements-20261023
tests / ci (push) Canceled after 0s
2026-10-23 16:35:03 +02:00
nala 603a88e1d3 Link documented investment complements to existing fiches and dossier context 2026-10-23 16:34:57 +02:00
paulomassigue 43cd0be7cb correcao de conflitos
tests / ci (push) Canceled after 0s
2026-10-23 16:03:34 +02:00
paulomassigue 89a9c3d994 mudanca o botao Tempo no hear fica Mais 2026-10-23 15:58:59 +02:00
nala 4098192a05 Record installed continuity corrections and destination delta checks 2026-10-23 15:48:52 +02:00
paulomassigue d242482042 home ajustado 2026-10-23 15:46:53 +02:00
nala 6b544548d3 Fix compressed draft saves provenance and Beira documentary continuity
tests / ci (push) Canceled after 0s
2026-10-23 15:41:37 +02:00
nala c3f7c3ad10 Record homologation product journey findings and publication status 2026-10-23 15:26:38 +02:00
nala 445aa7e69d Record consolidated adaptive portfolio installation receipt 2026-10-23 15:17:59 +02:00
nala 7c1f532134 Preserve inactive document history in adaptive reports
tests / ci (push) Canceled after 0s
2026-10-23 15:14:44 +02:00
nala 115c773162 Record QA continuity recovery after return reload and login
tests / ci (push) Canceled after 0s
2026-10-23 15:03:22 +02:00
paulomassigue ec4b2f2af2 organizacao da tela inicial 2026-10-23 14:51:34 +02:00
nala 3d877ec745 Preserve selected portfolio context through investment preparation 2026-10-23 14:35:50 +02:00
nala 6948ed62fc Consolidate adaptive preparation QA delivery and accepted evidence 2026-10-23 14:19:05 +02:00
nala 7d798579a6 Reconcile portfolio recovery evidence and integrated journey gap 2026-10-23 14:17:42 +02:00
nala 5bda74aedc Adapt land status to saved investment land need 2026-10-23 14:13:58 +02:00
nala f2150b1912 Reuse investment context as editable simulator summaries 2026-10-23 14:03:37 +02:00
nala 7ca94967f3 Adapt preparation presentation to declared applicability 2026-10-23 13:53:02 +02:00
paulomassigue e2b0fb4b2d melhorias de home 1 2026-10-23 10:36:34 +02:00
168 changed files with 14229 additions and 877 deletions
+8
View File
@@ -1,5 +1,13 @@
# Instruções permanentes do projeto InvesteMoz
## Percurso adaptativo — decisão expressa da Tempo, 23/10/2026
Carteira/projectos âncora e percurso adaptativo têm diagnósticos distintos, mas fazem parte da mesma experiência e responsabilidade de correcção. Separar o diagnóstico não autoriza adiamento, desligamento do percurso ou perda de contexto entre corredor/localização, oportunidades, associações documentadas a âncoras/clusters, dossier e simulador. A reposição prioritária só se considera demonstrada com causa delimitada e disponibilidade recuperada, reutilizando o inventário e os recibos existentes; melhorias noutro ponto não a comprovam. Consultar os registos de reposição antes de voltar a declarar causa desconhecida ou registos desaparecidos.
Apresentar apenas o necessário ao investimento em preparação, com entrada por ideia, oportunidade, sector ou localização, reutilizando informação válida e autorizada da oportunidade, perfil, organização, dossier e fontes anteriores. As etapas são adaptáveis, não seis formulários obrigatórios. Não sobrescrever edições deliberadas; explicar divergências e impactos antes da confirmação. Conservar escolhas ao avançar, regressar, actualizar e autenticar-se. Seleccionar opções únicas e pedir apenas dados em falta ou desactualizados.
Ocultar campos e acções sem aplicação, conservando explicação breve e histórico; desconhecido não significa não aplicável. Na selecção múltipla, partilhar dados comuns sem perder os requisitos específicos de cada oportunidade. Consulta pública livre; autenticação no momento de manifestar interesse ou iniciar operação privada. Relatórios e exportações devem reflectir o contexto e as etapas efectivamente aplicáveis. O utilizador deve perceber onde está, o que falta e a próxima acção. Aperfeiçoar os componentes existentes por incrementos e registar cobertura na matriz existente; esta instrução não declara concluída a sua implementação transversal.
## Regras permanentes do InvesteMoz — decisão da Tempo, 12/10/2026
1. O portal InvesteMoz é propriedade da Tempo, Lda.
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Services\InvestmentComplements;
use Illuminate\Http\Request;
final class InvestmentComplementController extends Controller
{
public function index(Request $request, InvestmentComplements $service)
{
$data = $request->validate(['origin' => [\Illuminate\Validation\Rule::requiredIf(!$request->has('contexts')), 'nullable', 'string', 'max:100'], 'territory_id' => 'nullable|uuid',
'contexts' => 'sometimes|array|max:100', 'contexts.*' => 'array:origin,territory_id', 'contexts.*.origin' => 'required|string|max:100', 'contexts.*.territory_id' => 'nullable|uuid']);
$items = [];
$issues = [];
foreach ($data['contexts'] ?? [['origin' => $data['origin'], 'territory_id' => $data['territory_id'] ?? null]] as $context) {
try {
$items = [...$items, ...$service->options($context['origin'], $context['territory_id'] ?? null)];
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException|\Symfony\Component\HttpKernel\Exception\HttpException $e) {
$status = $e instanceof \Symfony\Component\HttpKernel\Exception\HttpException ? $e->getStatusCode() : 404;
if (!isset($data['contexts']) || $status !== 404) throw $e;
$issues[] = ['origin' => $context['origin'], 'message' => 'Uma referência escolhida deixou de estar disponível para consulta pública. A relação guardada conserva-se no dossier; os outros complementos continuam disponíveis.'];
}
}
return response()->json(isset($data['contexts']) ? ['items' => $items, 'issues' => $issues] : $items)->header('Cache-Control', 'private, no-store');
}
}
@@ -89,7 +89,7 @@ class InvestmentDiscoveryController extends Controller
$data = $request->validate(['zone_id' => ['nullable', 'uuid'], 'from_dossier' => ['nullable', 'uuid']]);
$dossierOrigin = isset($data['from_dossier']) ? InvestmentPortfolioController::visible()->whereKey($data['from_dossier'])->firstOrFail() : null;
if ($dossierOrigin) {
$linked = false;
$linked = in_array($record->id, app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossierOrigin), true);
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
if ($dossierOrigin->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $record->id)->where($foreign, $dossierOrigin->$foreign)->exists()) {
$linked = true;
@@ -107,7 +107,7 @@ class InvestmentDiscoveryController extends Controller
$returnFilters = $request->query();
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
return Inertia::render('investment/detail', ['record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
return Inertia::render('investment/detail', ['complements' => app(\App\Services\InvestmentComplements::class)->options('opportunity:'.$record->id), 'record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
'guides' => app(\App\Services\SectorInvestment::class)->guides($record->sector),
'dossierOrigin' => $dossierOrigin ? ['id' => $dossierOrigin->id, 'title' => $dossierOrigin->title] : null,
'returnUrl' => $returnFilters === [] ? '/oportunidades' : '/oportunidades?'.http_build_query($returnFilters)]);
@@ -61,8 +61,15 @@ class InvestmentPortfolioController extends Controller
{
$management = $request->routeIs('management.portfolio*');
$filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'kind' => ['nullable', 'in:corridor,cluster,project'],
'sector' => ['nullable', 'string', 'max:500'],
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid']]);
$query = $management ? InvestmentDossier::query() : self::visible();
$sectorOptions = ($management ? InvestmentDossier::query() : self::visible())
->whereNotNull('content->sector')->get()->pluck('content.sector')
->filter(fn ($sector) => is_string($sector) && trim($sector) !== '')->unique()->sort()->values();
if (! empty($filters['sector'])) {
$query->where('content->sector', $filters['sector']);
}
if (! empty($filters['kind'])) {
$query->whereNotNull($filters['kind'].'_id');
}
@@ -76,7 +83,7 @@ class InvestmentPortfolioController extends Controller
}
return Inertia::render('portfolio/index', ['records' => $query->orderBy('title')->paginate(30)->withQueryString(),
'management' => $management, 'filters' => $filters, 'territorySelection' => $territorySelection]);
'management' => $management, 'filters' => $filters, 'sectors' => $sectorOptions, 'territorySelection' => $territorySelection]);
}
public function show(Request $request, string $id): Response
@@ -105,6 +112,7 @@ class InvestmentPortfolioController extends Controller
$opportunities = Opportunity::published()->where(function ($query) use ($record) {
$query->whereRaw('false');
$query->orWhereIn('id', app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record));
foreach (['clusters' => 'cluster_id', 'corridors' => 'corridor_id', 'projects' => 'project_id'] as $relation => $foreign) {
if ($record->$foreign) {
$query->orWhereIn('id', DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->select('opportunity_id'));
@@ -130,12 +138,19 @@ class InvestmentPortfolioController extends Controller
->orderByDesc('updated_at')->limit(10)->get(['id', 'version', 'updated_at'])
: collect();
return Inertia::render('portfolio/show', ['record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
return Inertia::render('portfolio/show', ['complements' => $management ? [] : app(\App\Services\InvestmentComplements::class)->options('portfolio:'.$record->id), 'record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
'anchorProject' => $anchorProject, 'existingSubmissions' => $existingSubmissions,
'geography' => app(InvestmentRelations::class)->dossier($record, $management),
'origin' => $origin ? ['id' => $origin->id, 'title' => $origin->title] : null, 'opportunities' => $opportunities, 'chains' => $chains]);
}
public function resumeInterest(Request $request, string $id): RedirectResponse
{
self::visible()->findOrFail($id);
$data = $request->validate(['catalogue_query' => ['nullable', 'string', 'max:4000']]);
return redirect('/carteira/'.$id.'?'.http_build_query([...$data, 'prepare_interest' => '1']));
}
public function save(Request $request, InvestmentDossier $dossier): RedirectResponse
{
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'title' => ['required', 'string', 'max:255'], 'source' => ['required', 'string', 'max:2000'],
@@ -192,11 +192,13 @@ class InvestmentSubmissionController extends Controller
{
$required = $submitting ? 'required' : 'nullable';
$data = $request->validate(['version' => ['required', 'integer', 'min:1'],
'payload' => ['required', 'array:context_reuse,full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload' => ['required', 'array:context_opportunity,context_opportunity_snapshot,context_reuse,full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload.organization_country_snapshot' => ['sometimes', 'array', 'max:10'],
'payload.residence_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.incorporation_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.other_details' => ['nullable', 'string', 'max:5000'],
'payload.full_name' => [$required, 'string', ...($submitting ? ['min:3'] : []), 'max:150'], 'payload.email' => [$required, $submitting ? 'email:rfc' : 'string', 'max:150'],
@@ -247,6 +249,9 @@ class InvestmentSubmissionController extends Controller
}
}
$bound = DB::table('desk_cases')->where('submission_id', $request->route('id'))->where('user_id', $request->user()->id)->whereNotNull('journey_id')->first();
if ($bound) $data['payload'] = app(\App\Services\InvestmentConsumerScope::class)->bind($request->user(), $bound->id, $data['payload'], $submitting);
else unset($data['payload']['context_opportunity'], $data['payload']['context_opportunity_snapshot']);
return $data;
}
@@ -266,7 +271,7 @@ class InvestmentSubmissionController extends Controller
$version = DB::transaction(function () use ($request, $id, $data) {
$row = $this->owned($request, $id, true, true);
abort_unless($row->status === 'draft' && $row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue antes de guardar.');
$row->update(['payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
$row->update([...(isset($data['payload']['context_opportunity']) ? ['opportunity_id'=>(int)substr($data['payload']['context_opportunity'],12)] : []), 'payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
InvestmentSpecificNeed::where('submission_id', $id)->delete();
foreach ($data['needs'] ?? [] as $need) {
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
@@ -310,7 +315,8 @@ class InvestmentSubmissionController extends Controller
return InterestRequest::findOrFail($row->interest_request_id)->reference;
}
abort_unless($row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue.');
$opportunity = $row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null;
$scopeKey = $data['payload']['context_opportunity'] ?? null;
$opportunity = $scopeKey ? Opportunity::published()->findOrFail(substr($scopeKey,12)) : ($row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null);
if ($row->dossier_id) {
InvestmentPortfolioController::visible()->findOrFail($row->dossier_id);
}
@@ -331,12 +337,14 @@ class InvestmentSubmissionController extends Controller
'sector_interest' => $p['sector'], 'message' => $p['objective'], 'privacy_consent' => true, 'consented_at' => now(),
'status' => 'received', 'source_page' => '/investidor/manifestacoes/'.$id,
'metadata' => ['submission_id' => $id, 'portal_user_id' => $request->user()->id, 'dossier_id' => $row->dossier_id,
'context_opportunity' => $p['context_opportunity'] ?? null,
'context_opportunity_snapshot' => $p['context_opportunity_snapshot'] ?? null,
'declared_origin_country_code' => $p['declared_origin_country_code'] ?? null,
'residence_country_code' => $p['residence_country_code'] ?? null,
'incorporation_country_code' => $p['incorporation_country_code'] ?? null,
...(isset($p['organization_country_snapshot']) ? ['organization_country_snapshot' => $p['organization_country_snapshot']] : []),
...($zoneOrigin ? ['zone_origin' => (array) $zoneOrigin] : [])]]));
$row->update(['payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
$row->update(['opportunity_id'=>$opportunity?->id, 'payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
InvestmentSpecificNeed::where('submission_id', $id)->delete();
foreach ($data['needs'] ?? [] as $need) {
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
@@ -19,12 +19,21 @@ class InvestmentTerritoryController extends Controller
$data = $request->validate(['territory_id' => ['required', 'uuid']]);
$profile = $request->is('minhas-oportunidades/*');
$management = $request->is('management/*') || $profile;
$actor = $request->user();
$access = app(PortalProfileAccess::class);
// The general consumers use this same endpoint while authenticated.
// Reuse the read access already granted by the private cadastral routes.
$profileAccess = $actor && $actor->hasVerifiedEmail()
&& ($access->grants($actor) !== [] || $access->superuser($actor));
$internal = $profileAccess || ($actor?->hasVerifiedEmail() && $actor->can('manage-portal'));
if ($management) {
$access = app(PortalProfileAccess::class);
abort_unless($profile ? ($access->grants($request->user()) !== [] || $access->superuser($request->user())) : $request->user()?->can('manage-portal'), 403);
abort_unless($profile ? $profileAccess : $internal, 403);
}
return response()->json(app(TerritoryGeometry::class)->context($data['territory_id'], $management));
return response()->json([
...app(TerritoryGeometry::class)->context($data['territory_id'], $internal),
'geometry_scope' => $internal ? 'authorized_internal' : 'public',
])->header('Cache-Control', 'private, no-store');
}
private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones'];
@@ -11,6 +11,36 @@ use Inertia\Inertia;
final class InvestorContextController extends Controller
{
private function entryComplements(Request $request): array
{
$data = $request->validate(['complement_origin' => 'nullable|string|max:100', 'complement_code' => 'required_with:complement_origin|string|max:40',
'complement_target' => 'nullable|string|max:100', 'complement_territory' => 'nullable|uuid']);
if (empty($data['complement_origin'])) return [];
return app(\App\Services\InvestmentComplements::class)->resolve([['origin' => $data['complement_origin'], 'code' => $data['complement_code'],
'target' => $data['complement_target'] ?? null, 'territory_id' => $data['complement_territory'] ?? null]]);
}
private function entryChoices(Request $r, InvestorContext $context): array
{
$data = $r->validate(['source' => 'nullable|string|max:100', 'from_dossier' => 'nullable|uuid', 'opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1',
'province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']);
$source = !empty($data['source']) ? $context->reference($data['source']) : null;
$ids = $data['opportunities'] ?? (($source['kind'] ?? '') === 'opportunity' ? [$source['id']] : []);
abort_if(!empty($data['from_dossier']) && !$ids, 422, 'Escolha a oportunidade associada à ficha da carteira.');
$path = app(InvestmentGeography::class)->selection($data);
$input = array_map(fn($id) => ['source' => 'opportunity:'.$id, ...($path ? ['territory' => 'territory:'.$path[array_key_last($path)]->id] : [])], $ids);
if (!empty($data['from_dossier'])) $input[0]['portfolio_origins'] = [$data['from_dossier']];
foreach ($this->entryComplements($r) as $complement) {
$keys = array_filter([$complement['origin']['key'], $complement['target']['key'] ?? null], fn($key) => str_starts_with($key, 'opportunity:'));
foreach ($keys as $key) {
if (in_array($key, array_column($input, 'source'), true)) continue;
$input[] = ['source' => $key, ...($complement['territory_id'] ? ['territory' => 'territory:'.$complement['territory_id']] : []),
...(str_starts_with($complement['origin']['key'], 'portfolio:') ? ['portfolio_origins' => [substr($complement['origin']['key'], 10)]] : [])];
}
}
return app(\App\Services\InvestmentOpportunityChoices::class)->resolve($input, [], true);
}
public function current(Request $r, InvestorContext $context)
{
return response()->json($context->current($r->user()));
@@ -23,7 +53,8 @@ final class InvestorContextController extends Controller
public function show(Request $r, string $id, InvestorContext $context)
{
return response()->json($context->view($r->user(), $id));
$data = $r->validate(['opportunity' => 'nullable|string|max:100']);
return response()->json($context->view($r->user(), $id, $data['opportunity'] ?? null));
}
public function options(Request $r, InvestorContext $context)
@@ -51,22 +82,38 @@ final class InvestorContextController extends Controller
$data = $context->view($r->user(), $id);
abort_unless($data['can_write'], 403);
$ids = $r->validate(['opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1'])['opportunities'] ?? [];
$choices = $ids ? app(\App\Services\InvestmentOpportunityChoices::class)->resolve(array_map(fn ($id) => ['source' => 'opportunity:'.$id], $ids)) : null;
$entries = $this->entryChoices($r, $context);
$choices = $entries ?: null;
$source = $r->query('source') ? $context->reference($r->validate(['source' => 'required|string|max:100'])['source']) : null;
if ($choices === null && ($source['kind'] ?? '') === 'opportunity') $choices = app(\App\Services\InvestmentOpportunityChoices::class)->resolve([['source' => $source['key']]]);
$explicitChoices=$choices;
$selectionKey=$r->validate(['selection_key'=>'nullable|uuid'])['selection_key']??null;
$entryKey=fn($c)=>$c['opportunity']['key'].($selectionKey?'@'.$selectionKey:'');
$entrySources=array_map($entryKey,$explicitChoices??[]);
if ($choices !== null) $choices = collect([...($data['context']['opportunity_choices'] ?? []), ...$choices])->unique(fn ($choice) => $choice['opportunity']['key'])->values()->all();
if ($choices !== null) $choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($data['context']['opportunity_choices'] ?? [], $choices);
$draft = $context->draft($r->user(), $id);
if ($draft && isset($draft['opportunity_choices'])) {
$recovered = app(\App\Services\InvestmentOpportunityChoices::class)->resolve($draft['opportunity_choices'], $data['context']['opportunity_choices'] ?? [], true);
$new=array_filter($explicitChoices??[],fn($c)=>!in_array($entryKey($c),$draft['entry_sources']??[],true));
$choices = collect([...$recovered, ...$new])->unique(fn($c)=>$c['opportunity']['key'])->values()->all();
$choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($recovered, array_values($new));
}
return Inertia::render('investor-path/context', ['entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : []]);
$differences = [];
foreach ($explicitChoices ?? [] as $entry) {
$kept = collect($choices ?? [])->firstWhere('opportunity.key', $entry['opportunity']['key']);
if ($kept && ($kept['territory']['key'] ?? null) !== ($entry['territory']['key'] ?? null)) $differences[] = ['title'=>$entry['opportunity']['title'],'saved'=>$kept['territory']['title']??'Por definir','requested'=>$entry['territory']['title']??'Por definir'];
}
$complements = isset($draft['complements']) ? app(\App\Services\InvestmentComplements::class)->resolve($draft['complements'], $data['context']['complements'] ?? []) : ($data['context']['complements'] ?? []);
$entryTerritory = null;
if (($source['kind'] ?? null) === 'portfolio') {
$path = app(InvestmentGeography::class)->selection($r->validate(['province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']));
if ($path) $entryTerritory = $context->reference('territory:'.$path[array_key_last($path)]->id);
}
foreach ($this->entryComplements($r) as $entry) {
$tag = 'complement:'.hash('sha256', $entry['key'].'@'.($selectionKey ?? ''));
$entrySources[] = $tag;
if (!in_array($tag, $draft['entry_sources'] ?? [], true)) $complements = collect([...$complements, $entry])->unique('key')->values()->all();
}
return Inertia::render('investor-path/context', ['entryTerritory' => $entryTerritory, 'entryComplements' => $complements, 'entryLocationDifferences'=>$differences,'entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : ($entryTerritory ? app(InvestmentGeography::class)->path($entryTerritory['id']) : [])]);
}
public function preview(Request $r, string $id, InvestorContext $context)
@@ -83,10 +130,16 @@ final class InvestorContextController extends Controller
{
$d = $r->validate(['source' => 'nullable|string|max:100', 'step' => ['nullable', 'regex:/^S(0[1-9]|1[0-9])$/']]);
$ref = isset($d['source']) ? $context->reference($d['source']) : null;
$submissionId = $r->validate(['submission' => 'nullable|uuid'])['submission'] ?? null;
$entrySubmission = null;
if ($submissionId) {
$submission = \App\Models\InvestmentSubmission::where('id', $submissionId)->where('user_id', $r->user()->id)->where('status', 'draft')->firstOrFail();
abort_unless(($ref['kind'] ?? null) === 'portfolio' && $submission->dossier_id === $ref['id'], 422, 'Conserve a ficha de origem da manifestação.');
$entrySubmission = ['id' => $submission->id, 'version' => $submission->version];
}
$ids = $r->validate(['opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1'])['opportunities'] ?? [];
$choices = $ids ? app(\App\Services\InvestmentOpportunityChoices::class)->resolve(array_map(fn ($id) => ['source' => 'opportunity:'.$id], $ids)) : [];
return Inertia::render('investor-path/context-start', ['opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]);
$choices = $this->entryChoices($r, $context);
return Inertia::render('investor-path/context-start', ['entrySubmission' => $entrySubmission, 'entryComplements' => $this->entryComplements($r), 'entryQuery' => $r->only(['complement_origin', 'complement_code', 'complement_target', 'complement_territory', 'from_dossier', 'province_id', 'district_id', 'admin_post_id', 'locality_id']), 'opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]);
}
public function installation(Request $r, InvestorContextInstallation $install)
@@ -88,7 +88,8 @@ class InvestorPathController extends Controller
public function show(Request $request, string $id, InvestorPath $path): JsonResponse|Response
{
$data = $path->view($request->user(), $id);
$choice = $request->validate(['opportunity' => 'nullable|string|max:100'])['opportunity'] ?? null;
$data = $path->view($request->user(), $id, $choice);
return $request->expectsJson() ? response()->json($data) : Inertia::render('investor-path/show', $data);
}
@@ -119,8 +119,8 @@ class VirtualDeskController extends Controller
public function createRequest(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
{
$data = $request->validate(['service_version_id' => 'required|uuid', 'client_key' => 'required|uuid']);
$row = $desk->createRequest($request->user(), $id, $data['service_version_id'], $data['client_key']);
$data = $request->validate(['service_version_id' => 'required|uuid', 'client_key' => 'required|uuid', 'opportunity' => 'nullable|string|max:100']);
$row = $desk->createRequest($request->user(), $id, $data['service_version_id'], $data['client_key'], $data['opportunity'] ?? null);
return $request->expectsJson() ? response()->json(['id' => $row->id, 'version' => $row->version], 201) : redirect('/balcao-virtual/pedidos/'.$row->id);
}
@@ -14,12 +14,13 @@ final class EnsureScopedPortalAdministration
public function handle(Request $request, Closure $next): Response
{
$actor = $request->user();
abort_unless($actor, 403);
abort_unless($actor !== null, 403);
$scope = app(ScopedPortalAdministration::class);
$path = $request->path();
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
$catalogue = $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
$geometryRead = $request->isMethod('GET') && $path === 'management/territory-geometries';
$catalogue = $geometryRead || $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
'management/economic-references/lot-01', 'management/economic-references/lot-01/*',
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
@@ -19,7 +19,9 @@ class StoreLandAssistanceCaseRequest extends FormRequest
{
$payload = $this->validated('payload');
return is_array($payload) ? $payload : [];
if (!is_array($payload)) return [];
if (!empty($payload['investor_path_case_id'])) return app(\App\Services\InvestmentConsumerScope::class)->bind($this->user(), $payload['investor_path_case_id'], $payload, $this->routeIs('land-assistance.support'));
return \Illuminate\Support\Arr::except($payload, ['context_opportunity', 'context_opportunity_snapshot']);
}
public function authorize(): bool
@@ -33,7 +35,9 @@ class StoreLandAssistanceCaseRequest extends FormRequest
return [
'version' => [$this->isMethod('post') && ! $this->routeIs('land-assistance.support') ? 'nullable' : 'required', 'integer', 'min:1'],
'context_revision' => ['nullable', 'integer', 'min:0'], 'context_reviewed' => ['nullable', 'boolean'],
'payload' => ['required', 'array:context_reuse,support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
'payload' => ['required', 'array:context_opportunity,context_opportunity_snapshot,context_reuse,support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.support_kind' => ['nullable', 'in:investment,home,family_exploitation,recognition,regularization,delimitation,change_of_use,transfer,existing_process,conflict_guidance,other'],
'payload.request_description' => ['nullable', 'string', 'max:4000'],
@@ -37,7 +37,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'contact_province', 'contact_district', 'preferred_channel',
];
$economic = [
'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'context_opportunity', 'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'competitor_assumption', 'sales_channel_assumption', 'marketing_approach', 'location_rationale', 'project_risk_responses_snapshot', 'project_questionnaire_version',
'customer_validation_status', 'cost_estimate_status', 'location_assessment_status', 'funding_status', 'receivables_timing_status',
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
@@ -73,6 +73,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.cashflow_snapshot' => ['nullable', 'string', 'max:20000'],
'economic_snapshot.context_applicability_snapshot' => ['nullable', 'string', 'max:1000000'],
'economic_snapshot.context_opportunity' => ['nullable', 'string', 'max:100'],
'economic_snapshot.investment_regime' => ['nullable', 'in:general,industrial_park,zee,zfi'],
'economic_snapshot.context_reuse' => ['nullable', 'string', 'max:10000'],
'economic_snapshot.compatible_fields_snapshot' => ['nullable', 'string', 'max:10000'],
@@ -0,0 +1,42 @@
<?php
namespace App\Services;
use RuntimeException;
/** Public read projection authorized by Tempo; source rows and originals stay private. */
class ApprovedAdministrativeBoundaries
{
public const HASH = '06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc';
/** @var array<string,array<string,string>>|null */
private ?array $items = null;
/** @return array<string,array<string,string>> */
public function snapshots(): array
{
if ($this->items !== null) {
return $this->items;
}
$contents = file_get_contents(base_path('docs/investmoz/unification/approved-administrative-boundaries-20261024.json'));
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', str_replace("\r\n", "\n", $contents)))) {
throw new RuntimeException('Approved administrative boundary manifest changed.');
}
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
if (count($manifest['items']) !== 408) {
throw new RuntimeException('Exact 408-boundary scope required.');
}
$this->items = array_column($manifest['items'], null, 'id');
return $this->items;
}
public function allows(string $id, string $territoryId, string $geometryHash, string $propertiesHash): bool
{
$item = $this->snapshots()[$id] ?? null;
return $item !== null && $item['territory_id'] === $territoryId
&& hash_equals($item['geometry_sha256'], $geometryHash)
&& hash_equals($item['properties_sha256'], $propertiesHash);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use RuntimeException;
/** Exact replay of the 28 accepted links into the transported project identities. */
final class ApprovedAnchorTerritories
{
public const HASH = 'd2893d0a638d630435aca1890f79a8094dc58eb65bbdd65a470bb685fa075190';
/** @return array{manifest_sha256:string,projects:list<array<string,mixed>>,links:list<array<string,mixed>>} */
public function plan(): array
{
$file = base_path('docs/investmoz/unification/approved-anchor-territories-20261023.json');
$contents = is_file($file) ? file_get_contents($file) : false;
if ($contents !== false) {
$contents = str_replace("\r\n", "\n", $contents);
}
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', $contents))) {
throw new RuntimeException('Reviewed territorial manifest changed.');
}
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
if (count($manifest['items']) !== 28) {
throw new RuntimeException('Exact 28-link scope required.');
}
$projects = [];
$links = [];
foreach ($manifest['items'] as $item) {
$project = DB::table('projects')->where('id', $item['project_id'])->whereNull('deleted_at')->first();
$dossier = DB::table('investment_dossiers')->where('id', $item['dossier_id'])->whereNull('deleted_at')->first();
if (! $project || $project->code !== $item['code'] || ! $dossier || $dossier->project_id !== $project->id
|| ! hash_equals($item['content_sha256'], hash('sha256', $dossier->content))
|| ! hash_equals($item['source_sha256'], hash('sha256', $dossier->source))) {
throw new RuntimeException('Project identity or documentary source changed: '.$item['code']);
}
$original = $item['original_relation'];
$geo = DB::table('geo_locations')->where('id', $original['geo_location_id'])->whereNull('deleted_at')->first();
foreach (['location_type', 'source_code', 'parent_id', 'data_classification'] as $field) {
if (! $geo || $item['territory'][$field] !== $geo->$field) {
throw new RuntimeException('Territory identity changed: '.$original['geo_location_id']);
}
}
$metadata = $projects[$project->id]['after_metadata'] ?? json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR);
$key = $geo->id.':'.$original['role'];
$evidence = $item['evidence'];
// Preserve the original dossier id in evidence and explicitly map the destination.
$evidence['transported_dossier_id'] = $dossier->id;
if (isset($metadata['documented_location_sources'][$key]) && $metadata['documented_location_sources'][$key] != $evidence) {
throw new RuntimeException('Existing territorial evidence differs; no overwrite: '.$item['code']);
}
$projects[$project->id] ??= ['id' => $project->id, 'code' => $project->code, 'before_version' => (int) $project->version_no, 'before_metadata' => json_decode($project->metadata, true, 512, JSON_THROW_ON_ERROR)];
$metadata['documented_location_sources'][$key] = $evidence;
$projects[$project->id]['after_metadata'] = $metadata;
$existing = DB::table('project_locations')->where('project_id', $project->id)->where('geo_location_id', $geo->id)->where('role', $original['role'])->first();
if ($existing && ($existing->deleted_at || $existing->data_classification !== 'public')) {
throw new RuntimeException('Archived or restricted territorial relation must not be revived.');
}
$identity = DB::table('project_locations')->where('id', $original['id'])->first();
if ($identity && (! $existing || $identity->id !== $existing->id)) {
throw new RuntimeException('Historical relation identity is already occupied.');
}
$links[] = ['code' => $item['code'], 'already_present' => (bool) $existing, 'row' => [...$original, 'project_id' => $project->id]];
}
return ['manifest_sha256' => self::HASH, 'projects' => array_values($projects), 'links' => $links];
}
/** Caller supplies an authenticated, backed-up transaction; no source publication changes.
* @param array{manifest_sha256:string,projects:list<array<string,mixed>>,links:list<array<string,mixed>>} $plan
* @return array<string,int>
*/
public function apply(array $plan): array
{
if (DB::transactionLevel() < 1 || $this->plan() !== $plan) {
throw new RuntimeException('Reviewed transaction and unchanged preview required.');
}
$inserted = 0;
$updated = 0;
foreach ($plan['projects'] as $project) {
if ($project['before_metadata'] == $project['after_metadata']) {
continue;
}
$count = DB::table('projects')->where('id', $project['id'])->where('version_no', $project['before_version'])->update([
'metadata' => json_encode($project['after_metadata'], JSON_THROW_ON_ERROR), 'version_no' => $project['before_version'] + 1, 'updated_at' => now(),
]);
if ($count !== 1) {
throw new RuntimeException('Concurrent project change.');
}
DB::table('audit_events')->insert(['source_system' => 'approved-anchor-territories-recovery-v1', 'external_id' => self::HASH.':'.$project['id'], 'entity_type' => 'projects', 'entity_id' => $project['id'], 'action' => 'accepted_territories_restored',
'data_classification' => 'public', 'before_data' => json_encode(['version_no' => $project['before_version'], 'metadata' => $project['before_metadata']], JSON_THROW_ON_ERROR),
'after_data' => json_encode(['version_no' => $project['before_version'] + 1, 'manifest_sha256' => self::HASH, 'metadata' => $project['after_metadata'], 'new_approval' => false], JSON_THROW_ON_ERROR)]);
$updated++;
}
foreach ($plan['links'] as $link) {
if ($link['already_present']) {
continue;
}
DB::table('project_locations')->insert($link['row']);
$inserted++;
}
return ['relations_restored' => $inserted, 'projects_updated' => $updated, 'new_dossiers' => 0, 'publication_changes' => 0];
}
}
@@ -13,6 +13,17 @@ use Ramsey\Uuid\Uuid;
final class CorridorAgribusinessOpportunities
{
public const SOURCE_HASH = '1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055';
/** Documentary cross-reference only; keep the two source designations and master records intact. */
public function portfolioOpportunityIds(\App\Models\InvestmentDossier $dossier): array
{
if (!$dossier->corridor_id || data_get($dossier->content, 'code') !== 'COR-02') return [];
// COR-02 is Beira e Centro in investment-portfolio.json. The seven entries are explicitly
// grouped under Corredor da Beira in the accepted source, not selected by province/name search.
$keys = collect($this->entries())->where('corridor', 'Corredor da Beira')->pluck('key');
return Opportunity::published()->whereIn('management_creation_key', $keys)
->where('workflow_context->corridor_agribusiness->source_sha256', self::SOURCE_HASH)->pluck('id')->all();
}
public const LIMITATIONS = 'Promotor, implantação exacta, área, montante, financiamento, viabilidade e licenças não indicados nesta fonte. O corredor é um enquadramento documental; não identifica uma parcela nem confirma disponibilidade.';
public function entries(): array
+4
View File
@@ -292,6 +292,10 @@ class InvestmentCatalogue
}
}
});
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)]));
}
return $ids;
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\Services;
use App\Http\Controllers\InvestmentPortfolioController;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
/** Resolve documented complements without creating opportunities or master relations. */
final class InvestmentComplements
{
private const TERMS = [
'irrigation' => ['Regadio', 'regadio|irrigação|irrigacao'],
'storage' => ['Silos e armazenagem', 'silos?|armazenagem|armazenamento'],
'cold' => ['Cadeia de frio', 'frio|refrigeração'],
'processing' => ['Processamento', 'processamento|transformação|moagem'],
'spinning' => ['Fiação', 'fiação|fiacao'],
'logistics' => ['Logística', 'logística|logistica|transporte'],
'certification' => ['Certificação', 'certificação|certificacao'],
'training' => ['Formação', 'formação|formacao'],
'finance' => ['Financiamento', 'financiamento|serviços financeiros'],
];
public function options(string $origin, ?string $territory = null): array
{
[$originKind, $originId] = array_pad(explode(':', $origin, 2), 2, '');
abort_unless(($originKind === 'portfolio' && Str::isUuid($originId)) || ($originKind === 'opportunity' && ctype_digit($originId)), 422, 'Origem complementar inválida.');
$principal = null;
$relationEvidence = [];
if (str_starts_with($origin, 'portfolio:')) {
$record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail(substr($origin, 10));
$text = trim(($record->content['focus'] ?? '')."\n".($record->content['gaps'] ?? '')."\n".($record->content['description'] ?? ''));
$reference = ['key' => $origin, 'title' => $record->title, 'href' => '/carteira/'.$record->id, 'version' => $record->version_no];
$ids = app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record);
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
if ($record->$foreign) $ids = [...$ids, ...DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->pluck('opportunity_id')->all()];
}
$basis = $record->source;
foreach (array_unique($ids) as $id) $relationEvidence[$id] = ['type' => 'portfolio', 'origin' => $origin, 'source' => $basis];
$serviceOrigins = [['dossier', $record->id]];
foreach (['project_id' => 'project', 'cluster_id' => 'cluster'] as $foreign => $type) {
if ($record->$foreign) $serviceOrigins[] = [$type, $record->$foreign];
}
} else {
$reference = app(InvestorContext::class)->reference($origin);
abort_unless($reference['kind'] === 'opportunity', 422);
$principal = Opportunity::published()->findOrFail($reference['id']);
$text = $principal->summary;
$basis = 'Resumo cadastrado da oportunidade '.$principal->reference;
$projects = DB::table('opportunity_projects')->where('opportunity_id', $principal->id)->pluck('project_id');
$links = DB::table('complementary_project_links')->whereIn('anchor_project_id', $projects)
->whereNull('deleted_at')->where('data_classification', 'public')->get(['id', 'anchor_project_id', 'complementary_project_id', 'relation_type', 'version_no']);
$targetProjects = DB::table('opportunity_projects')->whereIn('project_id', $links->pluck('complementary_project_id'))->get();
$ids = [$principal->id, ...$targetProjects->pluck('opportunity_id')->all()];
$relationEvidence[$principal->id] = ['type' => 'same_fiche', 'source' => $basis];
foreach ($targetProjects as $targetProject) $relationEvidence[$targetProject->opportunity_id] = ['type' => 'complementary_project',
'links' => $links->where('complementary_project_id', $targetProject->project_id)->values()->map(fn($link) => (array)$link)->all()];
foreach (Opportunity::published()->whereIn('id', $ids)->where('id', '!=', $principal->id)->get() as $linked) $text .= "\n".$linked->summary;
if (count($ids) > 1) $basis .= '; relações complementares públicas entre projectos';
$serviceOrigins = [['investment', (string) $principal->id]];
$classification = app(OpportunityClassification::class);
$principalChains = collect($classification->publicRows($principal))->where('target_type', 'chain')->filter(fn($r) => !empty($r['source_reference']));
$chains = $principalChains->pluck('target_id')->all();
if ($chains) {
Opportunity::published()->whereNot('id', $principal->id)->chunkById(100, function ($records) use (&$ids, &$relationEvidence, $chains, $principalChains, $classification) {
foreach ($records as $candidate) {
$related = collect($classification->publicRows($candidate))->where('target_type', 'chain')->filter(fn($r) => !empty($r['source_reference']));
$shared = array_intersect($chains, $related->pluck('target_id')->all());
if ($shared) {
$ids[] = $candidate->id;
$relationEvidence[$candidate->id] ??= ['type' => 'value_chain', 'principal' => $principalChains->whereIn('target_id', $shared)->values()->all(), 'target' => $related->whereIn('target_id', $shared)->values()->all()];
}
}
});
}
}
$territoryReference = $territory ? app(InvestorContext::class)->reference('territory:'.$territory) : null;
$candidates = Opportunity::published()->whereIn('id', array_unique($ids))->get();
$items = [];
foreach (self::TERMS as $code => [$label, $pattern]) {
if (!preg_match('/\b(?:'.$pattern.')\b/iu', $text, $match)) continue;
$offers = [];
foreach ($candidates as $candidate) {
// A common corridor alone does not prove the component. Keep the supporting excerpt.
if (!preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title."\n".$candidate->summary, $evidence)) continue;
// Mentioning a need for funding/training in a project's summary is not an offer.
if (in_array($code, ['certification', 'training', 'finance'], true) && !preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title)) continue;
if (in_array($code, ['certification', 'training', 'finance'], true) && preg_match('/\b(?:necessidade|pedido|procura)\b/iu', $candidate->title)) continue;
if ($territory && $candidate->primary_territory_id && !$this->compatible($territory, $candidate->primary_territory_id)) continue;
$offers[] = ['key' => 'opportunity:'.$candidate->id, 'title' => $candidate->title, 'href' => '/oportunidades/'.$candidate->slug,
'relationship' => $relationEvidence[$candidate->id] ?? null,
'version' => $candidate->management_version, 'evidence' => $evidence[0], 'location' => ($principal && $principal->id === $candidate->id ? 'Componente referido na própria ficha. ' : '').($candidate->primary_territory_id ? app(InvestorContext::class)->reference('territory:'.$candidate->primary_territory_id)['title'] : 'Implantação por definir')];
}
// Services are concrete published offerings explicitly associated with this source.
$services = [];
if (in_array($code, ['certification', 'training', 'finance'], true) && Schema::hasTable('partnership_origins') && Schema::hasTable('partnership_interests')) {
if ($serviceOrigins) {
$services = DB::table('portal_partnerships as p')->join('partnership_origins as o', 'o.partnership_id', '=', 'p.id')
->where(function ($q) use ($serviceOrigins) {
foreach ($serviceOrigins as [$type, $id]) $q->orWhere(fn($q) => $q->where('o.type', $type)->where('o.target_id', $id));
})->where('p.status', 'published')->where('p.kind', 'offering')
->where('p.partnership_type', $code === 'finance' ? 'finance' : 'training')
->when($code !== 'finance', fn($q) => $q->where(fn($q) => $q->where('p.title', 'ilike', '%'.$match[0].'%')->orWhereRaw("p.payload->>'summary' ILIKE ?", ['%'.$match[0].'%'])))->distinct()->get(['p.id', 'p.title'])
->filter(function ($p) use ($territory) {
if (!$territory) return true;
$locations = DB::table('partnership_locations')->where('partnership_id', $p->id)->pluck('geo_location_id');
return $locations->isEmpty() || $locations->contains(fn($id) => $this->compatible($territory, $id));
})->map(fn ($p) => ['key' => 'partnership:'.$p->id, 'title' => $p->title, 'href' => '/parcerias/'.$p->id, 'location' => 'Consultar âmbito na oferta'])->values()->all();
}
}
$excerpt = collect(explode("\n", $text))->first(fn($line) => preg_match('/\b(?:'.$pattern.')\b/iu', $line));
$items[] = ['code' => $code, 'title' => $label, 'origin' => $reference, 'territory_id' => $territory, 'territory' => $territoryReference,
'source' => $basis, 'excerpt' => $excerpt, 'source_sha256' => hash('sha256', $text), 'opportunities' => $offers, 'services' => $services];
}
return $items;
}
private function compatible(string $a, string $b): bool
{
if ($a === $b) return true;
$geo = app(InvestmentGeography::class);
return in_array($b, $geo->descendants($a), true) || in_array($a, $geo->descendants($b), true);
}
public function resolve(array $input, array $previous = []): array
{
$resolved = [];
foreach ($input as $choice) {
$target = $choice['target'] ?? null;
$old = collect($previous)->first(fn($r) => $r['origin']['key'] === $choice['origin'] && $r['code'] === $choice['code'] && ($r['target']['key'] ?? null) === $target && ($r['territory_id'] ?? null) === ($choice['territory_id'] ?? null));
try {
$item = collect($this->options($choice['origin'], $choice['territory_id'] ?? null))->firstWhere('code', $choice['code']);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
if (!$old) throw $e;
$item = null;
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
if (!$old || $e->getStatusCode() !== 404) throw $e;
$item = null;
}
$offer = $target && $item ? collect([...$item['opportunities'], ...$item['services']])->firstWhere('key', $target) : null;
if ($old && (!$item || ($target && !$offer) || (!$target && ($item['opportunities'] || $item['services'])))) {
$resolved[$old['key']] = [...$old, 'availability' => 'review_required'];
continue;
}
abort_unless($item, 422, 'Complemento sem referência documentada neste investimento.');
abort_if($target && !$offer, 422, 'A oferta complementar deixou de estar disponível neste contexto.');
abort_if(!$target && ($item['opportunities'] || $item['services']), 422, 'Já existe uma oferta relacionada; seleccione a ficha correspondente.');
$key = hash('sha256', $choice['origin'].'|'.$choice['code'].'|'.($target ?? 'idea').'|'.($choice['territory_id'] ?? ''));
$resolved[$key] = ['key' => $key, 'origin' => $item['origin'], 'code' => $item['code'], 'title' => $item['title'],
'kind' => $target ? (str_starts_with($target, 'opportunity:') ? 'opportunity' : 'service') : 'need',
'target' => $offer, 'territory_id' => $item['territory_id'], 'territory' => $item['territory'], 'source' => $item['source'], 'excerpt' => $item['excerpt'], 'source_sha256' => $item['source_sha256']];
}
return array_values($resolved);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Arr;
/** An identified opportunity within the authorised dossier, never the account's last choice. */
final class InvestmentConsumerScope
{
public function key(array $context, ?string $key, bool $required = false): ?string
{
$choices = $context['opportunity_choices'] ?? [];
$key = $key ?: (count($choices) === 1 ? $choices[0]['opportunity']['key'] : null);
abort_if($key && !collect($choices)->contains('opportunity.key', $key), 422, 'Escolha uma oportunidade deste dossier.');
abort_if($required && count($choices) > 1 && !$key, 422, 'Escolha a oportunidade em tratamento.');
return $key;
}
public function facts(array $payload, array $context, ?string $key): array
{
$key = $this->key($context, $key);
$path = $payload['investor_path'] ?? [];
$defaults = ['land_needed' => 'unknown', 'external_funding' => 'unknown', 'viability' => 'unknown'];
// A dossier-wide dispensation cannot decide a distinct opportunity's needs.
if ($key && count($context['opportunity_choices'] ?? []) > 1) return [...$defaults, ...($path['opportunity_facts'][$key] ?? [])];
if ($key && isset($path['opportunity_facts'][$key])) return [...$defaults, ...$path['opportunity_facts'][$key]];
if (count($context['opportunity_choices'] ?? []) > 1) return $defaults;
return [...$defaults, ...($path['facts'] ?? [])];
}
public function context(array $context, ?string $key): array
{
$choices = $context['opportunity_choices'] ?? [];
if (!$choices) return $context;
$key = $key ?: (count($choices) === 1 ? $choices[0]['opportunity']['key'] : null);
$choice = $key ? collect($choices)->firstWhere('opportunity.key', $key) : null;
abort_if($key && !$choice, 422, 'Escolha uma oportunidade deste dossier.');
$specific = ['project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'special_zone_id', 'investment_regime'];
$context['fields'] = Arr::except($context['fields'], $specific);
$context['provenance'] = Arr::except($context['provenance'] ?? [], $specific);
$context['selection'] = Arr::except($context['selection'], ['opportunity', 'territory', 'park']);
if (!$choice) {
$context['opportunity_choices'] = [];
$context['requirements_assessment']['requirements'] = [];
return $context;
}
$context['selection']['opportunity'] = $choice['opportunity'];
$context['fields'] = [...$context['fields'], ...Arr::only($choice['opportunity']['fields'], $specific)];
foreach (Arr::only($choice['opportunity']['fields'], $specific) as $field => $value) $context['provenance'][$field] = Arr::only($choice['opportunity'], ['key','kind','id','title','version','sha256']);
unset($context['selection']['sector']);
if (!empty($context['fields']['sector_id'])) $context['selection']['sector'] = app(InvestorContext::class)->reference('sector:'.$context['fields']['sector_id']);
if ($choice['territory']) {
$context['selection']['territory'] = $choice['territory'];
$context['fields'] = [...Arr::except($context['fields'], array_keys(InvestmentGeography::FIELDS)), ...$choice['territory']['fields']];
foreach ($choice['territory']['fields'] as $field => $value) $context['provenance'][$field] = Arr::only($choice['territory'], ['key','kind','id','title','version','sha256']);
} else {
$context['fields'] = Arr::except($context['fields'], ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location']);
}
$park = $choice['park'] ?? null;
if ($park) $context['selection']['park'] = $park;
$context['fields']['special_zone_id'] = $park['id'] ?? '';
$context['fields']['investment_regime'] = $park['fields']['investment_regime'] ?? 'general';
$context['opportunity_choices'] = [$choice];
$context['requirements_assessment']['requirements'] = array_values(array_filter($context['requirements_assessment']['requirements'] ?? [], fn($r) => $r['opportunity_key'] === $key));
$context['consumer_opportunity'] = $choice;
$context['complements'] = array_values(array_filter($context['complements'] ?? [], fn($c) => ($c['origin']['key'] ?? null) === $key || ($c['target']['key'] ?? null) === $key || in_array(substr($c['origin']['key'] ?? '',10),array_column($choice['portfolio_origins'] ?? [],'id'),true)));
$context['dependency_hash'] = app(InvestorContext::class)->dependencyHash(['opportunity_choices'=>[$choice], 'fields'=>Arr::except($context['fields'],['project_title','project_summary']), 'complements'=>$context['complements']]);
return $context;
}
public function bind(User $actor, string $id, array $payload, bool $required = false): array
{
$case = app(VirtualDesk::class)->case($actor, $id, true);
$context = app(InvestorContext::class)->state($case);
$scoped = $this->context($context, $payload['context_opportunity'] ?? null);
$choice = $scoped['consumer_opportunity'] ?? null;
abort_if($required && count($context['opportunity_choices'] ?? []) > 1 && !$choice, 422, 'Escolha a oportunidade em tratamento antes de submeter.');
if (!$choice) return Arr::except($payload, ['context_opportunity', 'context_opportunity_snapshot']);
// Canonical server snapshot; client text cannot establish a different origin or territory.
$payload['context_opportunity'] = $choice['opportunity']['key'];
$payload['context_opportunity_snapshot'] = json_encode(['context_id' => $id, 'revision' => $context['revision'], 'choice' => $choice, 'requirements' => $scoped['requirements_assessment'] ?? null], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
return $payload;
}
}
+45 -2
View File
@@ -2,11 +2,42 @@
namespace App\Services;
use App\Http\Controllers\InvestmentPortfolioController;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
/** Multiple identified references in the existing encrypted investment context. */
final class InvestmentOpportunityChoices
{
public function portfolioOrigins(array $ids, int $opportunity): array
{
$origins = [];
foreach (array_unique($ids) as $id) {
$record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail($id);
$kind = null;
if (in_array($opportunity, app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record), true)) $kind = 'corridor';
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
if ($record->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $opportunity)->where($foreign, $record->$foreign)->exists()) $kind = rtrim($relation, 's');
}
abort_unless($kind, 422, 'A associação escolhida não pertence a esta oportunidade.');
$origins[] = ['id' => $record->id, 'title' => $record->title, 'kind' => $kind, 'href' => '/carteira/'.$record->id,
'version' => $record->version_no, 'sha256' => hash('sha256', json_encode([$record->title, $record->source, $record->content], JSON_THROW_ON_ERROR))];
}
return $origins;
}
/** Add explicitly consulted associations without resetting saved geography or deliberate answers. */
public function mergeEntries(array $saved, array $entries): array
{
foreach ($entries as $entry) {
$index = array_search($entry['opportunity']['key'], array_column(array_column($saved, 'opportunity'), 'key'), true);
if ($index === false) { $saved[] = $entry; continue; }
$origins = collect([...($saved[$index]['portfolio_origins'] ?? []), ...($entry['portfolio_origins'] ?? [])])->unique('id')->values()->all();
if ($origins) $saved[$index]['portfolio_origins'] = $origins;
}
return $saved;
}
public function resolve(array $choices, array $previous = [], bool $pending = false): array
{
$result = [];
@@ -15,11 +46,12 @@ final class InvestmentOpportunityChoices
abort_unless($ref['kind'] === 'opportunity', 422, 'Seleccione uma ficha de oportunidade.');
$record = Opportunity::findOrFail($ref['id']);
$old = collect($previous)->first(fn ($row) => $row['opportunity']['key'] === $ref['key']);
$origins = $this->portfolioOrigins($choice['portfolio_origins'] ?? array_column($old['portfolio_origins'] ?? [], 'id'), (int) $record->id);
$corridors = app(InvestmentCorridorContext::class);
$corridor = $choice['corridor_key'] ?? $old['corridor_key'] ?? $corridors->key($record);
$provinces = $corridor ? $corridors->provinces($corridor) : [];
$documented = $record->primary_territory_id;
$territory = $choice['territory'] ?? ($documented ? 'territory:'.$documented : null);
$territory = array_key_exists('territory', $choice) ? $choice['territory'] : ($documented ? 'territory:'.$documented : null);
if (!$territory && count($provinces) === 1) $territory = 'territory:'.$provinces[0]->id;
if ($corridor && $territory) abort_unless($corridors->compatible($corridor, substr($territory, 10)), 422, 'A localização não pertence ao corredor escolhido.');
$previousTerritory = $old['territory']['key'] ?? null;
@@ -33,7 +65,18 @@ final class InvestmentOpportunityChoices
}
$location = $territory ? app(InvestorContext::class)->reference($territory) : null;
abort_unless($location === null || $location['kind'] === 'territory', 422);
$result[] = ['opportunity' => $ref, 'territory' => $location,
$parkKey = array_key_exists('park', $choice) ? $choice['park'] : ($old['park']['key'] ?? null);
$park = $parkKey ? app(InvestorContext::class)->reference($parkKey) : null;
if ($park) {
$eligible = $corridor ? array_column($corridors->parks($corridor, $location['id'] ?? null), 'id') : [];
if (!in_array($park['id'], $eligible, true)) {
abort_if(!empty($choice['park']), 422, 'A zona ou parque não é compatível com a localização desta oportunidade.');
$park = null;
}
abort_unless(!$park || $park['kind'] === 'park', 422);
}
$result[] = ['opportunity' => $ref, 'territory' => $location, ...($origins ? ['portfolio_origins' => $origins] : []),
'park' => $park,
'corridor_key' => $corridor, 'corridor_name' => $corridor ? config('investment_corridors.'.$corridor.'.name') : null,
'allowed_provinces' => array_map(fn($p)=>(array)$p, $provinces),
'territory_path' => $location ? array_map(fn ($node) => \Illuminate\Support\Arr::only((array) $node, ['id', 'name', 'parent_id', 'location_type']), app(InvestmentGeography::class)->path($location['id'], true)) : [],
+94 -16
View File
@@ -39,10 +39,11 @@ final class InvestorContext
$case=$this->desk->case($actor,$id,true);
abort_if($case->closed_at,409);
$sequence = (int)($input['draft_sequence'] ?? 0);
$entrySources=Validator::make($input,['entry_sources'=>'sometimes|array|max:100','entry_sources.*'=>'string|max:100'])->validate()['entry_sources']??[];
$entrySources=Validator::make($input,['entry_sources'=>'sometimes|array|max:200','entry_sources.*'=>'string|max:100'])->validate()['entry_sources']??[];
unset($input['draft_sequence'],$input['entry_sources']);
abort_unless($sequence > 0, 422);
$data = $this->input($input);
if (isset($data['complements'])) app(InvestmentComplements::class)->resolve($data['complements'], $this->state($case)['complements'] ?? []);
foreach ($data['selection'] as $slot=>$key) {
if (!$key) continue;
$ref=$this->reference($key);
@@ -52,6 +53,7 @@ final class InvestorContext
$ref = $this->reference($choice['source']);
abort_unless($ref['kind'] === 'opportunity', 422);
$record = Opportunity::findOrFail($ref['id']);
app(InvestmentOpportunityChoices::class)->portfolioOrigins($choice['portfolio_origins'] ?? [], (int) $record->id);
$key = $choice['corridor_key'] ?? app(InvestmentCorridorContext::class)->key($record);
if (!empty($choice['territory'])) {
$location = $this->reference($choice['territory']);
@@ -59,6 +61,7 @@ final class InvestorContext
if ($key) abort_unless(app(InvestmentCorridorContext::class)->compatible($key, $location['id']), 422, 'A localização não pertence ao corredor escolhido.');
}
}
if (isset($data['opportunity_choices'])) app(InvestmentOpportunityChoices::class)->resolve($data['opportunity_choices'], $this->state($case)['opportunity_choices'] ?? [], true);
if (!empty($data['selection']['park'])) {
$first=$data['opportunity_choices'][0] ?? null;
$source=$first['source'] ?? $data['selection']['opportunity'] ?? null;
@@ -109,6 +112,7 @@ final class InvestorContext
}
}
}
if (!empty($context['opportunity_choices']) && !array_key_exists('park', $context['opportunity_choices'][0])) $context['opportunity_choices'][0]['park'] = $context['selection']['park'] ?? null;
$zone = $context['selection']['park'] ?? null;
$context['fields']['investment_regime'] = $zone['fields']['investment_regime'] ?? 'general';
$context['fields']['special_zone_id'] = $zone['id'] ?? '';
@@ -118,6 +122,8 @@ final class InvestorContext
public function dependencyHash(array $selection): string
{
unset($selection['portfolio'], $selection['selection']['portfolio']);
if (isset($selection['opportunity_choices'])) $selection['opportunity_choices'] = array_map(fn($choice)=>Arr::except($choice,['portfolio_origins']),$selection['opportunity_choices']);
return hash('sha256', json_encode(Arr::sortRecursive($selection), JSON_THROW_ON_ERROR));
}
@@ -163,6 +169,18 @@ final class InvestorContext
if ($o->primary_territory_id) {
$fields = [...$fields, ...$this->territoryFields($o->primary_territory_id)];
}
} elseif ($kind === 'portfolio') {
abort_unless(Str::isUuid($id), 422);
$dossier = \App\Http\Controllers\InvestmentPortfolioController::visible()->findOrFail($id);
$title = $dossier->title;
$href = '/carteira/'.$id;
// Documentary coverage is not the investor's chosen implantation.
$version = DB::table('investment_dossiers')->where('id', $id)->value('version_no');
$fields = ['project_title' => $title, 'project_summary' => $dossier->content['description'] ?? $dossier->content['focus'] ?? ''];
$state = 'documented_origin';
$zoneAnalysis = ['source' => $dossier->source, 'documented_sector' => $dossier->content['sector'] ?? null,
'documented_location' => $dossier->content['location'] ?? $dossier->content['region'] ?? null,
'geography' => app(InvestmentRelations::class)->dossier($dossier), 'available_opportunity' => false];
} elseif ($kind === 'business') {
abort_unless(config('existing-business.enabled') && Str::isUuid($id), 404);
$o = ExistingBusiness::published()->with('sector')->findOrFail($id);
@@ -326,7 +344,7 @@ final class InvestorContext
private function input(array $input): array
{
abort_if(array_diff(array_keys($input), ['opportunity_choices', 'version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
abort_if(array_diff(array_keys($input), ['complements', 'opportunity_choices', 'version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
abort_if(! app(PrivateInvestmentPreparation::class)->available() && (! empty($input['document_links']) || array_intersect(array_keys(is_array($input['declarations'] ?? null) ? $input['declarations'] : []), PrivateInvestmentPreparation::FIELDS)), 409, 'Esta preparação ainda não está disponível neste ambiente. As respostas foram preservadas.');
@@ -339,12 +357,17 @@ final class InvestorContext
}
return Validator::make($input, [...$inputRules,
'opportunity_choices' => 'sometimes|array|max:100', 'opportunity_choices.*' => 'array:source,territory,location_reason,confirm_location_change,activities,corridor_key',
'complements' => 'sometimes|array|max:100', 'complements.*' => 'array:origin,code,target,territory_id',
'complements.*.origin' => 'required|string|max:100', 'complements.*.code' => 'required|string|max:40',
'complements.*.target' => 'nullable|string|max:100', 'complements.*.territory_id' => 'nullable|uuid',
'opportunity_choices' => 'sometimes|array|max:100', 'opportunity_choices.*' => 'array:source,territory,location_reason,confirm_location_change,activities,corridor_key,portfolio_origins,park',
'opportunity_choices.*.portfolio_origins' => 'sometimes|array|max:100', 'opportunity_choices.*.portfolio_origins.*' => 'uuid',
'opportunity_choices.*.corridor_key' => ['sometimes', 'nullable', Rule::in(array_keys(config('investment_corridors')))],
'opportunity_choices.*.activities' => 'sometimes|array', 'opportunity_choices.*.activities.*' => 'string|distinct|in:production,processing,trade,transport,water,forestry,animal',
'opportunity_choices.*.park' => 'sometimes|nullable|string|max:100',
'opportunity_choices.*.source' => 'required|string|distinct|max:100', 'opportunity_choices.*.territory' => 'nullable|string|max:100',
'opportunity_choices.*.location_reason' => 'nullable|string|max:2000', 'opportunity_choices.*.confirm_location_change' => 'sometimes|boolean',
'version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
'version' => 'required|integer|min:1', 'selection' => 'present|array:portfolio,opportunity,sector,territory,park',
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage,'.implode(',', PrivateInvestmentPreparation::FIELDS), 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
'declarations.currency' => 'nullable|regex:/^[A-Z]{3}$/', 'declarations.expected_start_date' => 'nullable|date_format:Y-m-d', 'declarations.investor_origin' => 'nullable|in:national,foreign,mixed', 'declarations.person_type' => 'nullable|in:singular,collective,sole_trader,consortium', 'declarations.entity_stage' => 'nullable|in:existing,being_formed', 'declarations.tax_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.fiscal_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.project_type' => 'nullable|string|max:100', 'declarations.maturity_stage' => 'nullable|string|max:100',
'replace_fields' => 'nullable|array', 'replace_fields.*' => 'string|in:'.implode(',', self::FIELDS)])->validate();
@@ -358,6 +381,7 @@ final class InvestorContext
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão atual.');
app(InvestorPath::class)->view($actor, $id);
$old = $this->state($case);
$complements = array_key_exists('complements', $data) ? app(InvestmentComplements::class)->resolve($data['complements'], $old['complements'] ?? []) : ($old['complements'] ?? []);
$links = $old['preparation_documents'] ?? [];
foreach ($data['document_links'] ?? [] as $key => $document) {
if ($document) {
@@ -381,10 +405,17 @@ final class InvestorContext
$data['selection']['opportunity'] = $choices[0]['opportunity']['key'] ?? null;
// Shared fields concern the first identified opportunity; other sites remain separate.
$data['selection']['territory'] = $choices[0]['territory']['key'] ?? null;
if (array_key_exists('park', $data['opportunity_choices'][0] ?? [])) $data['selection']['park'] = $choices[0]['park']['key'] ?? null;
} elseif (array_key_exists('opportunity', $data['selection']) && $choices !== []) {
abort(422, 'Actualize a selecção identificada de oportunidades; não substitua silenciosamente o conjunto por uma ficha.');
}
$selection = $old['selection'];
foreach ($complements as $complement) {
if (($complement['availability'] ?? '') === 'review_required') continue;
foreach ([$complement['origin']['key'], $complement['target']['key'] ?? ''] as $key) {
if (str_starts_with($key, 'opportunity:')) abort_unless(in_array($key, array_column(array_column($choices, 'opportunity'), 'key'), true), 422, 'Conserve a oportunidade escolhida ou retire explicitamente a sua relação complementar.');
}
}
foreach ($data['selection'] as $slot => $key) {
if ($key === null || $key === '') {
unset($selection[$slot]);
@@ -414,7 +445,7 @@ final class InvestorContext
}
$proposed = [];
$origins = [];
foreach (['opportunity', 'sector', 'park', 'territory'] as $slot) {
foreach (['portfolio', 'opportunity', 'sector', 'park', 'territory'] as $slot) {
if (in_array($slot, ['park', 'territory'], true) && array_intersect(array_keys($selection[$slot]['fields'] ?? []), array_keys(InvestmentGeography::FIELDS))) {
foreach (['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location'] as $key) {
unset($proposed[$key],$origins[$key]);
@@ -466,18 +497,21 @@ final class InvestorContext
$replaced[] = ['field' => $key, 'before' => $old['fields'][$key] ?? null, 'after' => $fields[$key] ?? null];
}
}
foreach (['opportunity', 'sector', 'territory', 'park'] as $slot) {
foreach (['portfolio', 'opportunity', 'sector', 'territory', 'park'] as $slot) {
if (($old['selection'][$slot] ?? null) !== ($selection[$slot] ?? null)) {
$changed[] = $slot;
}
}
$changedFields = array_column($replaced, 'field');
if ($complements !== ($old['complements'] ?? [])) $changed[] = 'complements';
$choicesChanged = $choices !== ($old['opportunity_choices'] ?? []);
if ($choicesChanged) $changed = array_values(array_unique([...$changed, 'opportunity', 'territory']));
$choiceInputsChanged = array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$choices) !== array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$old['opportunity_choices']??[]);
if ($choiceInputsChanged) $changed = array_values(array_unique([...$changed, 'opportunity', 'territory']));
elseif ($choicesChanged) $changed[] = 'portfolio_origins';
$conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary']));
$dependencyChanged = $choicesChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
$dependencyChanged = $choiceInputsChanged || in_array('complements', $changed, true) || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
$privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS);
$calculationInputsChanged = $choicesChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
$calculationInputsChanged = $choiceInputsChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
$payload = $this->desk->decode($case->payload);
$results = $payload['investor_path']['results'] ?? [];
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
@@ -488,6 +522,7 @@ final class InvestorContext
if ($privateConditionsChanged) {
$steps = array_values(array_unique([...$steps, 'S02', 'S05', 'S06', 'S07']));
}
if (in_array('complements', $changed, true)) $steps = array_values(array_unique([...$steps, 'S02', 'S08']));
$privatePreparation = null;
if (app(PrivateInvestmentPreparation::class)->available()) {
$docs = $this->preparationDocuments($id);
@@ -495,7 +530,7 @@ final class InvestorContext
$afterPreparation = app(PrivateInvestmentPreparation::class)->assess(['fields' => $fields, 'selection' => $selection, 'preparation_documents' => $links], $docs);
$privatePreparation = ['before' => $beforePreparation, 'after' => $afterPreparation];
}
$plan = ['opportunity_choices' => $choices, 'case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
$plan = ['complements' => $complements, 'opportunity_choices' => $choices, 'case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
'preparation_documents' => $links, 'private_investment' => $privatePreparation,
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $calculationInputsChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'calculation_inputs_changed' => $calculationInputsChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
@@ -540,8 +575,9 @@ final class InvestorContext
$review[$step] = ['revision' => $revision, 'reason' => 'O contexto mudou; reveja os pressupostos e a evidência aplicável.'];
}
$context = ['opportunity_choices' => $plan['opportunity_choices'], 'preparation_documents' => $plan['preparation_documents'], 'preparation_snapshot' => $plan['private_investment']['after'] ?? $before['preparation_snapshot'] ?? null, 'revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
'dependency_hash' => $this->dependencyHash([...($plan['opportunity_choices'] !== [] ? ['opportunity_choices' => $plan['opportunity_choices']] : []), 'selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
'dependency_hash' => $this->dependencyHash([...($plan['opportunity_choices'] !== [] ? ['opportunity_choices' => $plan['opportunity_choices']] : []), ...($plan['complements'] ? ['complements' => $plan['complements']] : []), 'selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
$context['requirements_assessment'] = $plan['requirements_assessment'];
$context['complements'] = $plan['complements'];
if ($plan['calculation_inputs_changed']) {
foreach ($payload['investor_path']['results'] ?? [] as $i => $r) {
$payload['investor_path']['results'][$i] = [...$r, 'context_state' => 'review_required', 'context_invalidated_revision' => $revision, 'context_invalidated_at' => now()->toIso8601String()];
@@ -558,12 +594,13 @@ final class InvestorContext
}, 3);
}
public function view(User $actor, string $id): array
public function view(User $actor, string $id, ?string $opportunity = null): array
{
$case = $this->desk->case($actor, $id);
abort_unless($this->desk->owns($actor, $case, false) || $actor->can('manage-portal'), 403);
$ctx = $this->state($case);
$path = app(InvestorPath::class)->view($actor, $id);
$ctx['land_preparation'] = $this->landPreparation($case, $opportunity);
$path = app(InvestorPath::class)->view($actor, $id, $opportunity);
$ctx['path_applicability'] = ['definition_id' => $path['definition_id'], 'definition_version' => $path['definition_version'], 'source_sha256' => $path['source_sha256'],
'steps' => array_values(array_map(fn ($step) => Arr::only($step, ['code', 'title', 'applicability', 'reason', 'state', 'context_review_required']), array_filter($path['steps'], fn ($step) => ! empty($step['branch']))))];
@@ -578,12 +615,19 @@ final class InvestorContext
}
}
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($ctx, $this->preparationDocuments($id)) : null;
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess(app(InvestmentConsumerScope::class)->context($ctx, $opportunity), $this->preparationDocuments($id)) : null;
$ctx['preparation_assessment'] = $preparation;
$prefill = $this->prefill($ctx, $opportunity);
$saved = $this->desk->decode($case->payload);
if (!empty($saved['reuse_consent'])) {
$contacts = array_filter(['email' => $saved['email'] ?? null, 'phone' => $saved['profile']['phone'] ?? null], fn($v) => is_string($v) && $v !== '');
$prefill['simulator'] = [...$contacts, ...$prefill['simulator']];
$prefill['land'] = [...$contacts, ...(!empty($saved['name']) ? ['full_name' => $saved['name']] : []), ...$prefill['land']];
}
return ['private_investment' => $preparation, 'preparation_documents' => $this->preparationDocuments($id), 'simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
'can_write' => $path['canWrite'], 'next' => $path['next'], 'results' => $path['results'], 'requests' => $path['requests'],
'prefill' => $this->prefill($ctx), 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
'prefill' => $prefill, 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
}
public function preparationDocuments(string $id): array
@@ -591,23 +635,57 @@ final class InvestorContext
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->whereNull('d.deleted_at')->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256'])->map(fn ($row) => (array) $row)->all();
}
public function prefill(array $ctx): array
public function landPreparation(\stdClass $case, ?string $opportunity = null): array
{
$payload = $this->desk->decode($case->payload);
$ctx = $this->state($case);
$key = app(InvestmentConsumerScope::class)->key($ctx, $opportunity);
$path = $payload['investor_path'] ?? [];
$need = app(InvestmentConsumerScope::class)->facts($payload, $ctx, $key)['land_needed'];
return ['need' => in_array($need, ['yes', 'no'], true) ? $need : 'unknown', 'reason' => $key ? ($path['opportunity_fact_reasons'][$key]['land_needed'] ?? '') : ($path['fact_reasons']['land_needed'] ?? '')];
}
public function prefill(array $ctx, ?string $opportunity = null): array
{
$original = $ctx;
$ctx = app(InvestmentConsumerScope::class)->context($ctx, $opportunity);
$f = $ctx['fields'];
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]);
// Entering this tool from an investment dossier selects the existing investment-analysis route.
// The reconciliation preserves any purpose deliberately chosen in an existing simulator draft.
$sim += ['journey_type' => 'investor', 'journey_type_label' => 'Avaliar o meu investimento', 'purpose' => 'simulate_only', 'purpose_label' => 'Apenas simular'];
if (($ctx['land_preparation']['need'] ?? null) === 'yes') {
$tenure = ['duat' => ['duat', 'DUAT'], 'lease' => ['leased', 'Arrendado']][$f['site_tenure'] ?? ''] ?? null;
if ($tenure) [$sim['land_status'], $sim['land_status_label']] = $tenure;
}
// Human labels travel with the exact existing identifiers, not inferred locations.
foreach (app(InvestmentGeography::class)->publicReferences()->whereIn('id', array_values(Arr::only($f, array_keys(InvestmentGeography::FIELDS))))->get(['id', 'name']) as $place) {
foreach (array_keys(InvestmentGeography::FIELDS) as $key) {
if (($f[$key] ?? null) === $place->id) $sim[$key.'_label'] = $place->name;
}
}
if (! empty($f['sector_id']) && ($sector = Sector::where('is_active', true)->find($f['sector_id']))) $sim['sector_id_label'] = $sector->name;
$zone = $ctx['selection']['park'] ?? null;
$sim['context_applicability_snapshot'] = json_encode([
'version' => 1, 'context_revision' => $ctx['revision'],
'consumer_opportunity' => $ctx['consumer_opportunity']['opportunity']['key'] ?? null,
'portfolio_origin' => $ctx['selection']['portfolio'] ?? null,
'zone' => $zone ? Arr::only($zone, ['id', 'title', 'href', 'version', 'sha256', 'fields', 'analysis']) : null,
'provenance' => $ctx['provenance'],
'opportunity_choices' => $ctx['opportunity_choices'] ?? [],
'complements' => $ctx['complements'] ?? [],
'requirements_assessment' => $ctx['requirements_assessment'] ?? null,
'path' => $ctx['path_applicability'] ?? null,
'land_preparation' => $ctx['land_preparation'] ?? null,
'field_provenance' => array_map(fn($origin) => Arr::only($origin, ['kind', 'title', 'version', 'context_revision']), $ctx['provenance'] ?? []),
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$scoped = app(InvestmentConsumerScope::class)->context($original, $opportunity);
$f = $scoped['fields'];
return ['simulator' => $sim, 'land' => array_filter(['project_reference' => $f['project_title'] ?? null, 'activity' => $f['sector'] ?? null, 'purpose' => $f['project_summary'] ?? null, 'parcels' => [Arr::only($f, array_keys(InvestmentGeography::FIELDS))]], fn ($v) => $v !== null),
'desk' => array_filter(['title' => $f['project_title'] ?? null, 'territory_id' => $f['locality_id'] ?? $f['admin_post_id'] ?? $f['district_id'] ?? $f['province_id'] ?? null, 'location_source' => isset($f['province_id']) ? 'Referência consultada no contexto v'.($ctx['revision'] ?? 0).': '.($f['location'] ?? 'território identificado').'. Não confirma disponibilidade.' : null, 'description' => $f['project_summary'] ?? null, 'sector' => $f['sector'] ?? null, ...Arr::only($f, array_keys(InvestmentGeography::FIELDS))], fn ($v) => $v !== null),
'submission' => array_filter(['currency' => $f['currency'] ?? null, 'sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
+38 -16
View File
@@ -68,7 +68,7 @@ final class InvestorPath
}
/** @return array<string,mixed> */
public function view(User $actor, string $id): array
public function view(User $actor, string $id, ?string $opportunity = null): array
{
abort_unless($this->available(), 404);
$case = $this->desk->case($actor, $id);
@@ -79,18 +79,26 @@ final class InvestorPath
$payload = $this->desk->decode($case->payload);
$path = $payload['investor_path'] ?? [];
$context = app(InvestorContext::class)->state($case);
$facts = $path['facts'] ?? ['land_needed' => 'unknown', 'external_funding' => 'unknown', 'viability' => 'unknown'];
$scope = app(InvestmentConsumerScope::class);
$opportunity = $scope->key($context, $opportunity);
$allChoices = $context['opportunity_choices'] ?? [];
$facts = $scope->facts($payload, $context, $opportunity);
$factReasons = $opportunity ? ($path['opportunity_fact_reasons'][$opportunity] ?? []) : ($path['fact_reasons'] ?? []);
$context = $scope->context($context, $opportunity);
$records = DB::table('journey_steps')->where('journey_id', $journey->id)->whereNull('deleted_at')->get()->keyBy('step_code');
$steps = [];
foreach ($definition['steps'] as $step) {
$row = $records->get($step['code']);
abort_unless($row !== null, 409, 'Etapa em falta; reparar a instalação sem reconstruir o histórico.');
$detail = $this->desk->decode($row->path_detail);
if ($opportunity && (count($allChoices) > 1 || isset($detail['opportunities'][$opportunity]))) {
$detail = $detail['opportunities'][$opportunity] ?? ['state' => 'not_started'];
}
$value = $step['branch'] ? ($facts[$step['branch']] ?? 'unknown') : 'yes';
$applicable = $value === 'unknown' ? 'undetermined' : ($value === 'no' ? 'not_needed_for_preparation' : 'applicable');
$state = $value === 'no' ? 'dispensed' : $detail['state'];
$steps[] = [...$step, 'id' => $row->id, 'version' => $row->version_no, 'state' => $state, 'saved_state' => $detail['state'],
'applicability' => $applicable, 'reason' => $value === 'no' ? ($path['fact_reasons'][$step['branch']] ?? '') : ($detail['reason'] ?? ''),
'applicability' => $applicable, 'reason' => $value === 'no' ? ($factReasons[$step['branch']] ?? '') : ($detail['reason'] ?? ''),
'detail' => $detail, 'preparation_allowed' => $value !== 'no',
'action_gaps' => []];
}
@@ -143,7 +151,8 @@ final class InvestorPath
$landCase = (int) $case->user_id === $actor->id
? LandAssistanceCase::where('user_id', $actor->id)->latest('updated_at')->get()
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id) : null;
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id
&& (!$allChoices || ($opportunity && ($land->payload['context_opportunity'] ?? null) === $opportunity))) : null;
$preparation = ($owner || $manager) && app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($context, $documents->map(fn ($d) => (array) $d)->all()) : null;
if ($preparation && $preparation['enabled']) {
@@ -152,11 +161,12 @@ final class InvestorPath
return ['private_investment' => $preparation, 'case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
'journey_id' => $journey->id, 'definition_id' => $version->id, 'definition_version' => $version->version_no, 'source_sha256' => $definition['source_sha256'],
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $path['fact_reasons'] ?? [],
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $factReasons,
'context_opportunity' => $opportunity, 'opportunity_options' => $allChoices,
'fintech' => ($owner || $manager) && (isset($path['fintech']) || collect($path['sector_contexts'] ?? [])->contains(fn ($s) => in_array($s['slug'], ['fintech', 'tecnologias-de-informacao', 'mercado-financeiro'], true)) || collect($path['content_sources'] ?? [])->contains('return_href', '/tech-fintech')) ? app(FintechInvestment::class)->view($payload, $steps) : null,
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => $next['href'] ?? '#'.$next['code']] : null,
'investment_context' => ($owner || $manager) ? $context : null,
'investment_context' => ($owner || $manager) ? [...$context, 'opportunity_choices' => $opportunity ? $context['opportunity_choices'] : $allChoices] : null,
'results' => $results, 'requests' => $requests, 'documents' => $documents,
'zone_contexts' => ($owner || $manager) ? ($path['zone_contexts'] ?? []) : [],
'sector_contexts' => ($owner || $manager) ? ($path['sector_contexts'] ?? []) : [],
@@ -167,25 +177,31 @@ final class InvestorPath
->get(['id', 'title', 'status', 'version_no', 'due_at', 'assignee_user_id']),
'services' => DB::table('desk_service_versions')->where('status', 'published')->orderByDesc('version')->get(['id', 'code', 'definition'])
->unique('code')->values()->map(fn ($s) => ['id' => $s->id, 'title' => json_decode($s->definition, true)['title']]),
'tools' => ['simulator' => (int) $case->user_id === $actor->id ? '/simulador-fiscal?context='.$id.($case->simulation_id ? '&draft='.$case->simulation_id : '') : null,
'desk' => '/balcao-virtual/dossiers/'.$id, 'guidance' => '/balcao-virtual/dossiers/'.$id.'/orientacao',
'tools' => ['simulator' => (int) $case->user_id === $actor->id ? '/simulador-fiscal?context='.$id.($case->simulation_id ? '&draft='.$case->simulation_id : '').($opportunity ? '&opportunity='.urlencode($opportunity) : '') : null,
'desk' => '/balcao-virtual/dossiers/'.$id.($opportunity ? '?opportunity='.urlencode($opportunity) : ''), 'guidance' => '/balcao-virtual/dossiers/'.$id.'/orientacao',
'library' => '/documentos?context='.$id, 'opportunities' => '/oportunidades?context='.$id, 'territory' => '/zonas?context='.$id, 'partners' => '/parcerias/painel?context='.$id,
'land' => (int) $case->user_id === $actor->id ? ($landCase ? '/investidor/terra-duat/'.$landCase->id : '/investidor/terra-duat/novo?context='.$id) : null]];
'land' => ($facts['land_needed'] ?? 'unknown') !== 'no' && (int) $case->user_id === $actor->id ? ($landCase ? '/investidor/terra-duat/'.$landCase->id : '/investidor/terra-duat/novo?context='.$id.($opportunity ? '&opportunity='.urlencode($opportunity) : '')) : null]];
}
/** @param array<string,mixed> $input */
public function facts(User $actor, string $id, array $input): void
{
$data = Validator::make($input, ['version' => 'required|integer|min:1',
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'context_opportunity' => 'nullable|string|max:100',
'land_needed' => ['required', Rule::in(['yes', 'no', 'unknown'])], 'external_funding' => ['required', Rule::in(['yes', 'no', 'unknown'])],
'viability' => ['required', Rule::in(['unknown', 'proceed', 'reformulate', 'archive'])],
'reason' => 'required|string|min:3|max:3000'])->validate();
DB::transaction(function () use ($actor, $id, $data): void {
$case = $this->writable($actor, $id, $data['version']);
$payload = $this->desk->decode($case->payload);
$choice = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), $data['context_opportunity'] ?? null, true);
foreach (['land_needed', 'external_funding', 'viability'] as $key) {
$payload['investor_path']['facts'][$key] = $data[$key];
$payload['investor_path']['fact_reasons'][$key] = $data['reason'];
if ($choice) {
$payload['investor_path']['opportunity_facts'][$choice][$key] = $data[$key];
$payload['investor_path']['opportunity_fact_reasons'][$choice][$key] = $data['reason'];
} else {
$payload['investor_path']['facts'][$key] = $data[$key];
$payload['investor_path']['fact_reasons'][$key] = $data['reason'];
}
}
$this->savePayload($case, $payload);
$this->desk->event($this->desk->case($actor, $id), $actor, 'path_applicability_declared', $data);
@@ -195,7 +211,7 @@ final class InvestorPath
/** @param array<string,mixed> $input */
public function stage(User $actor, string $id, string $code, array $input): void
{
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'state' => ['required', Rule::in(array_diff(InvestorPathDefinition::STATES, ['dispensed']))],
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'context_opportunity' => 'nullable|string|max:100', 'state' => ['required', Rule::in(array_diff(InvestorPathDefinition::STATES, ['dispensed']))],
'reason' => 'required|string|min:3|max:3000', 'action' => ['required', Rule::in(['prepare', 'submit', 'execute', 'operate', 'review', 'record_external'])],
'document_id' => 'nullable|uuid', 'authority' => 'nullable|string|max:200', 'reference' => 'nullable|string|max:300',
'context_revision' => 'nullable|integer|min:0', 'context_reviewed' => 'nullable|boolean', 'decision_date' => 'nullable|date_format:Y-m-d|before_or_equal:today'])->validate();
@@ -203,7 +219,8 @@ final class InvestorPath
$case = $this->desk->case($actor, $id, false, true);
abort_if($case->closed_at, 409);
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão actual.');
$view = $this->view($actor, $id);
$key = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), $data['context_opportunity'] ?? null, true);
$view = $this->view($actor, $id, $key);
$step = $this->findStep($view, $code);
abort_unless($step !== null && $step['preparation_allowed'], 422, 'Etapa dispensada de preparação; altere os pressupostos com fundamento.');
$state = $data['state'];
@@ -238,7 +255,10 @@ final class InvestorPath
}
abort_if($state === 'completed' && ($step['context_review_required'] ?? false) && (! (bool) ($data['context_reviewed'] ?? false) || ($data['context_revision'] ?? -1) !== ($view['investment_context']['revision'] ?? 0)), 422, 'O contexto desta etapa exige nova conferência fundamentada; o registo anterior permanece no histórico.');
$detail = [...$data, 'context_dependency_hash' => $view['investment_context']['dependency_hash'] ?? null, 'fintech_fingerprint' => $view['fintech']['review_fingerprints'][$code] ?? null, 'scope' => $scope, 'actor_id' => $actor->id, 'recorded_at' => now()->toIso8601String()];
DB::table('journey_steps')->where('id', $step['id'])->update(['path_detail' => $this->desk->encode($detail),
$stored = $this->desk->decode(DB::table('journey_steps')->where('id', $step['id'])->value('path_detail'));
if ($key) $stored['opportunities'][$key] = $detail;
else $stored = [...$stored, ...$detail];
DB::table('journey_steps')->where('id', $step['id'])->update(['path_detail' => $this->desk->encode($stored),
'status' => in_array($state, ['completed', 'approved'], true) ? 'completed' : ($state === 'not_started' ? 'pending' : 'active'),
'version_no' => $step['version'] + 1, 'updated_at' => now()]);
DB::table('desk_cases')->where('id', $id)->update(['version' => $case->version + 1, 'updated_at' => now()]);
@@ -267,11 +287,12 @@ final class InvestorPath
abort_unless(is_array($parsed) && ($parsed['version'] ?? null) === 1 && is_array($parsed['data'] ?? null), 422, 'Snapshot financeiro incompatível.');
$payload = $this->desk->decode($case->payload);
$context = app(InvestorContext::class)->state($case);
$opportunity = app(InvestmentConsumerScope::class)->key($context, $economic['context_opportunity'] ?? null, true);
if ($context['revision'] > 0) {
abort_unless(($data['context_revision'] ?? -1) === $context['revision'], 409, 'O contexto mudou; recupere e confira os pressupostos antes de devolver o resultado.');
abort_unless($data['context_reviewed'] ?? false, 422, 'Confirme a revisão dos pressupostos financeiros para este contexto.');
}
$resultKey = hash('sha256', $draft->id.':'.$draft->version_no.':'.$snapshot.':context:'.$context['revision']);
$resultKey = hash('sha256', $draft->id.':'.$draft->version_no.':'.$snapshot.':context:'.$context['revision'].($opportunity ? ':'.$opportunity : ''));
foreach ($payload['investor_path']['results'] ?? [] as $result) {
if ($result['source_key'] === $resultKey) {
return $result;
@@ -280,6 +301,7 @@ final class InvestorPath
abort_unless((int) $case->version === $data['version'], 409, 'Recupere a versão actual do dossier.');
$result = ['id' => (string) Str::uuid(), 'source_key' => $resultKey, 'draft_id' => $draft->id, 'draft_version' => $draft->version_no,
'snapshot_sha256' => $draft->snapshot_sha256, 'financial_snapshot' => $snapshot,
'context_opportunity' => $opportunity,
'currency' => $economic['currency'] ?? null, 'engine' => 'financial-calculators:v1',
'kind' => 'local_calculation_inputs', 'context_state' => 'current', 'context_revision' => $context['revision'], 'context_dependency_hash' => $context['dependency_hash'] ?? app(InvestorContext::class)->dependencyHash([]), 'recorded_at' => now()->toIso8601String()];
$payload['investor_path']['results'][] = $result;
+37 -1
View File
@@ -60,9 +60,11 @@ final class SimulationFormDrafts
throw new HttpException(409, 'Já existe um rascunho activo. Carregue-o antes de guardar novas alterações.');
}
if ($case) $data['economic_snapshot'] = $this->landContext($data['economic_snapshot'], $case);
$snapshotHash = hash('sha256', json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$draft = SimulationFormDraft::query()->create([
'id' => (string) Str::uuid(), 'owner_user_id' => $actorId, 'anonymous_token_hash' => $tokenHash,
'version_no' => 1, 'create_key_hash' => $keyHash, 'create_payload_hash' => $payloadHash, 'snapshot_sha256' => $payloadHash,
'version_no' => 1, 'create_key_hash' => $keyHash, 'create_payload_hash' => $payloadHash, 'snapshot_sha256' => $snapshotHash,
'personal_snapshot' => SimulationFormDraft::encryptSnapshot($data['personal_snapshot']),
'economic_snapshot' => SimulationFormDraft::encryptSnapshot($data['economic_snapshot']),
'expires_at' => $actorId === null ? now()->addDays(30) : null,
@@ -89,6 +91,7 @@ final class SimulationFormDrafts
$this->checkContext($request, $draft);
$this->requireMatch($request, $draft);
$previousEconomic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
$data['economic_snapshot'] = $this->preserveInactiveLandStatus($draft, $data['economic_snapshot'], $previousEconomic, $actorId);
if (($data['economic_snapshot']['reuse_simulation'] ?? null) === 'no') {
unset($data['economic_snapshot']['simulation_sources']);
} elseif (is_array($previousEconomic['simulation_sources'] ?? null)) {
@@ -456,6 +459,39 @@ final class SimulationFormDrafts
return $query;
}
private function preserveInactiveLandStatus(SimulationFormDraft $draft, array $economic, array $previous, ?int $actorId): array
{
if (! $actorId || ! config('investor-path.enabled') || ! app(VirtualDesk::class)->installed()) return $economic;
// Use the real owner-bound dossier even if a request omits its context parameter.
$case = DB::table('desk_cases')->where('simulation_id', $draft->id)->where('user_id', $actorId)->first();
if (! $case) return $economic;
return $this->landContext($economic, $case, $previous);
}
private function landContext(array $economic, \stdClass $case, ?array $previous = null): array
{
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? $previous['context_applicability_snapshot'] ?? '', true);
$key = $economic['context_opportunity'] ?? $snapshot['consumer_opportunity'] ?? null;
$key = app(InvestmentConsumerScope::class)->key(app(InvestorContext::class)->state($case), is_string($key) ? $key : null);
$decision = app(InvestorContext::class)->landPreparation($case, $key);
if ($decision['need'] !== 'yes' && $previous !== null) {
if (array_key_exists('land_status', $previous)) $economic['land_status'] = $previous['land_status'];
else unset($economic['land_status']);
if (isset($previous['selection_labels']['land_status'])) $economic['selection_labels']['land_status'] = $previous['selection_labels']['land_status'];
else unset($economic['selection_labels']['land_status']);
}
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? $previous['context_applicability_snapshot'] ?? '', true);
if (! is_array($snapshot) || ($snapshot['version'] ?? null) !== 1) {
$snapshot = ['version' => 1, 'context_revision' => app(InvestorContext::class)->state($case)['revision']];
}
$snapshot['land_preparation'] = $decision;
$snapshot['consumer_opportunity'] = $key;
if ($key) $economic['context_opportunity'] = $key;
$economic['context_applicability_snapshot'] = json_encode($snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
return $economic;
}
private function checkContext(Request $request, SimulationFormDraft $draft): void
{
$context = $request->input('context');
+19 -4
View File
@@ -14,16 +14,31 @@ final class TerritoryGeometry
abort_unless($geography->references($management)->where('id', $id)->exists(), 404);
$path = $geography->path($id, $management);
$ids = array_map(fn ($node) => $node->id, $path);
$approved = app(ApprovedAdministrativeBoundaries::class);
$rows = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
->whereIn('g.geo_location_id', $ids)->whereNull('g.deleted_at')->whereNull('l.deleted_at')
->whereIn('g.data_classification', $management ? ['internal', 'public'] : ['public'])
->where(function ($query) use ($management, $approved) {
$query->where('g.data_classification', 'public');
$query->orWhere(function ($internal) use ($management, $approved) {
$internal->where('g.data_classification', 'internal');
if (! $management) {
$internal->whereIn('g.id', array_keys($approved->snapshots()));
}
});
})
->whereRaw("g.properties->>'purpose' = 'administrative_boundary'")
->whereIn('g.geometry_type', ['polygon', 'multipolygon'])
->whereRaw('ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_SRID(g.geom)=4326')
->orderBy('g.geo_location_id')->orderBy('g.id')->limit(101)
->get(['g.id', 'g.geo_location_id', 'l.name', 'l.location_type', 'g.properties', 'g.data_classification',
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry')]);
$areas = $rows->take(100)->map(function ($row) use ($id) {
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry'),
DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256")]);
if (! $management) {
$rows = $rows->filter(fn ($row) => $row->data_classification === 'public'
|| $approved->allows($row->id, $row->geo_location_id, $row->geometry_sha256, $row->properties_sha256))->values();
}
$areas = $rows->take(100)->map(function ($row) use ($id, $management) {
$source = json_decode($row->properties, true, 512, JSON_THROW_ON_ERROR);
return ['id' => $row->id, 'territory_id' => $row->geo_location_id, 'location_type' => $row->location_type,
@@ -31,7 +46,7 @@ final class TerritoryGeometry
'title' => 'Limite administrativo: '.$row->name, 'purpose' => 'administrative_boundary',
'source_reference' => $source['authority'] ?? null, 'source_date' => $source['source_version'] ?? null,
'source_url' => $source['source_url'] ?? null, 'source_sha256' => $source['source_sha256'] ?? null,
'data_classification' => $row->data_classification, 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
'data_classification' => $management ? $row->data_classification : 'public', 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
})->all();
return ['territory_id' => $id, 'territory_registered' => true,
+18 -4
View File
@@ -170,11 +170,11 @@ class VirtualDesk
});
}
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey): \stdClass
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey, ?string $opportunity = null): \stdClass
{
abort_unless(Str::isUuid($serviceId) && Str::isUuid($clientKey), 422);
return DB::transaction(function () use ($actor, $caseId, $serviceId, $clientKey) {
return DB::transaction(function () use ($actor, $caseId, $serviceId, $clientKey, $opportunity) {
$case = $this->case($actor, $caseId, true, true);
abort_if($case->closed_at, 409, 'Dossier encerrado.');
$service = DB::table('desk_service_versions')->where('id', $serviceId)->where('status', 'published')->first();
@@ -197,7 +197,10 @@ class VirtualDesk
$this->event($case, $actor, 'request_prefill_prepared', ['draft_id' => $draft->id, 'draft_version' => $draft->version_no, 'snapshot_sha256' => $draft->snapshot_sha256, 'fields' => ['title', 'description']]);
}
$context = app(InvestorContext::class)->state($case);
$mapped = app(InvestorContext::class)->prefill($context)['desk'];
if (count($context['opportunity_choices'] ?? []) > 1 && !$opportunity) $payload['description'] = '';
$mapped = app(InvestorContext::class)->prefill($context, $opportunity)['desk'];
if ($opportunity) $payload['context_opportunity'] = $opportunity;
$payload = app(InvestmentConsumerScope::class)->bind($actor, $caseId, $payload);
$serviceFields = json_decode($service->definition, true)['fields'];
foreach (['territory_id' => 'territory_id', 'location_source' => 'location_source', 'title' => 'title', 'description' => 'description', 'sector' => 'sector', 'province_id' => 'province_id', 'district_id' => 'district_id', 'admin_post_id' => 'admin_post_id', 'locality_id' => 'locality_id'] as $field => $source) {
if (isset($serviceFields[$field],$mapped[$source]) && (empty($payload[$field]) || in_array($field, ['title', 'description'], true) && $context['revision'] > 0)) {
@@ -230,6 +233,12 @@ class VirtualDesk
$draft = SimulationFormDraft::query()->whereKey($draft->id)->lockForUpdate()->firstOrFail();
}
$economic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
$contextChoices = app(InvestorContext::class)->state($case)['opportunity_choices'] ?? [];
if (count($contextChoices) > 1) {
$snapshot = json_decode($economic['context_applicability_snapshot'] ?? '{}', true);
$draftChoices = $snapshot['opportunity_choices'] ?? [];
if (count($draftChoices) !== 1 || ($draftChoices[0]['opportunity']['key'] ?? null) !== ($payload['context_opportunity'] ?? null)) return null;
}
$description = (string) ($economic['project_summary'] ?? '');
if (trim($description) === '') {
return null;
@@ -250,7 +259,9 @@ class VirtualDesk
* @return array<string,mixed> */
public function validateFields(array $payload, array $definition, bool $submitting): array
{
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code', 'context_reuse'])],
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code', 'context_reuse', 'context_opportunity', 'context_opportunity_snapshot'])],
'payload.context_opportunity' => ['nullable', 'string', 'max:100'],
'payload.context_opportunity_snapshot' => ['nullable', 'string', 'max:200000'],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.country' => ['nullable', 'string', 'max:100', app(CountryCatalogue::class)->legacyCountryRule()],
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()]];
@@ -334,6 +345,9 @@ class VirtualDesk
}
}
$payload = $this->validateFields($data['payload'], $this->definition($request), $submit);
if ($request->status !== 'returned' || in_array('context_opportunity', json_decode($request->editable_fields ?? '[]', true), true)) {
$payload = app(InvestmentConsumerScope::class)->bind($actor, $request->case_id, $payload, $submit);
}
if ($request->status === 'returned') {
$editable = json_decode($request->editable_fields ?? '[]', true);
$previous = $this->decode($request->payload);
@@ -0,0 +1,710 @@
{
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: aplicar o anexo de identidade visual aos cinco cartões de Encontre o contexto do seu investimento.",
"instructionSource": {
"path": "C:\\Users\\jorge\\.codex\\attachments\\637b878f-1f58-490d-bd07-d5cd10ae2377\\Pasted text.txt",
"sha256": "fcc24dcce4b5df3f10e29add48052808d48ddcf674d052287854e11910e01d69"
},
"source": {
"type": "AI-generated photorealistic illustrative images",
"tool": "image_gen, built-in mode",
"skill": "C:/Users/jorge/.codex/skills/.system/imagegen/SKILL.md",
"externalPhotographsUsed": false,
"namedRealLocationsDepicted": false,
"note": "Cenas ilustrativas plausíveis, sem atribuição a instalações, pessoas, instituições ou projectos reais."
},
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#fotografias-nos-cartões-de-contexto-do-investimento--23102026",
"filesChanged": [
"resources/js/pages/PortalInvestidor.tsx",
"resources/js/components/investment-context-cards.tsx",
"tools/portal-context-cards-qa.mjs",
"docs/investmoz/public-portal-sections.md",
"docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
"docs/investmoz/public-portal-context-images.json"
],
"images": [
{
"key": "clusters-agroindustria",
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a modest, well maintained agro-processing packing facility linking commercial agriculture to local industry. Wide three-quarter view into an airy open-sided processing shed with pale walls and a few orderly wooden or reusable produce crates, stainless processing/sorting benches and an unobtrusive commercial agricultural field visible beyond. Two Black southern African workers in clean practical workwear sorting harvested vegetables naturally, faces not prominent. Show production and transformation together in ONE genuine-looking continuous scene, no montage. Clear simple geometry, warm-neutral sunlight, subtle vegetation.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f72e66b0-d788-4476-8b20-c39b22d5d6dc.png",
"original": "outputs/investment-context-20261023/originals/clusters-agroindustria.png",
"originalSha256": "75e04a694081ab757831fc9b37323949d09806e622c83d07588fc47c423c9033",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-context/clusters-agroindustria.webp",
"width": 960,
"height": 540,
"bytes": 116796,
"sha256": "12494b31ab551cffb79f81c7d1519c0c4a2d4236e7f129bec1ce32251b39d847"
},
{
"file": "public/images/investment-context/clusters-agroindustria-480.webp",
"width": 480,
"height": 270,
"bytes": 42060,
"sha256": "76e3152c99ed188f748542dcf0464787307dbdc1c6299e7264dfd1bd86ad75e3"
},
{
"file": "public/images/investment-context/clusters-agroindustria-fallback.webp",
"width": 240,
"height": 135,
"bytes": 9596,
"sha256": "9f668da50f7d64955b31940d239b420ac18dbb76a8120c8bedab3f2c04710288"
}
]
},
{
"key": "corredores-logisticos",
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a commercial freight terminal serving a southern African logistics corridor. Elevated medium-wide viewpoint showing an orderly short line of unbranded freight containers, one realistic container-handling crane and a freight truck on a paved lane, with a calm strip of water and low coastal industrial vegetation in the distance. Infrastructure of credible modest contemporary scale, generic and unidentifiable; do not reproduce or name Maputo, Beira, Nacala or any real port. Spatially coherent containers, road and handling equipment; clean clear subject with no excessive machinery.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-0363da23-f9cb-413b-bb64-024539085c4b.png",
"original": "outputs/investment-context-20261023/originals/corredores-logisticos-v2.png",
"originalSha256": "2151d1a59265c6f7151d18af6ef6ad78f9fc6e0cd58ac9cc5c64b50a25c5058e",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-context/corredores-logisticos-v2.webp",
"width": 960,
"height": 540,
"bytes": 85184,
"sha256": "1939c26df297588b6c3e5ea08b700d4cda5d1183cf3e5faed06a1ca060d2475e"
},
{
"file": "public/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 480,
"height": 270,
"bytes": 26222,
"sha256": "06dc0e7126cb2c13eb3f9a98f6ee58f5d64f9d07c28620975f744f263744fe15"
},
{
"file": "public/images/investment-context/corredores-logisticos-v2-fallback.webp",
"width": 240,
"height": 135,
"bytes": 5334,
"sha256": "f9fd6e6c3a2d4654df1a8fdb287e122f1889b5d318c104c4816ee393c793e242"
}
],
"selectedSource": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-c2cff7da-d602-4191-9aca-380918a09619.png",
"editPrompt": "Edit the provided photograph: REMOVE THE ENTIRE FOREGROUND TRUCK, including cab, wheels, long trailer and the red freight container on that trailer. The big truck at lower left/centre must be completely gone. Replace the removed truck's area with a continuous empty realistic paved road surface matching the surrounding pavement and road markings. Leave the stacked stationary freight containers along the quay, the water, the large port crane, the lifted container and workers. All remaining equipment and containers should have plain surfaces without readable lettering, logos, numbers or brands. No new truck, vehicle, symbol, text or overlay. Keep a single clean editorial photograph, natural lighting and horizontal 16:9 framing. Generic commercial port scene, not a named real facility."
},
{
"key": "projectos-ancora",
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a small team of two Black southern African engineering professionals, one woman and one man, in realistic hard hats and restrained protective workwear, seen candidly from a three-quarter rear angle reviewing an unmarked folded technical plan at a utility-scale solar installation. Low rows of realistic blue solar panels and a simple low service building recede into a dry green southern African landscape. No readable drawing or numbers on the plan. Focus on credible engineering and investment project scale; clean composition, quiet professional atmosphere, no landmark or company identifiers.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-19fcd882-542b-43c9-81e7-593fd75083f6.png",
"original": "outputs/investment-context-20261023/originals/projectos-ancora.png",
"originalSha256": "f23cf6e64307a280f3aa58741454a23480ac28fcae01a0810adf134b783a3a74",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-context/projectos-ancora.webp",
"width": 960,
"height": 540,
"bytes": 77114,
"sha256": "de0b59774246fee982761de59618b3e7707c3fe30f2b1bcbb6017d4dce53c4dc"
},
{
"file": "public/images/investment-context/projectos-ancora-480.webp",
"width": 480,
"height": 270,
"bytes": 29484,
"sha256": "268e405112ed341fce3c9792c3caa47053984100cd4a204453e31bbe5db6e890"
},
{
"file": "public/images/investment-context/projectos-ancora-fallback.webp",
"width": 240,
"height": 135,
"bytes": 6750,
"sha256": "db7e4b4382e09eb6b4f9f2a4603ccac4b4eacc53ba0f6776429e33dbf5131954"
}
]
},
{
"key": "educacao-formacao",
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a candid technical training classroom with three Black southern African young adult students of different genders and one instructor working together at a practical electronics and computing workbench. Bright modest well maintained vocational classroom, natural window light, simple laboratory equipment and unbranded closed or angled laptops whose screens have no visible interface or text. Focus on two students engaged in a realistic small hands-on electronics exercise with normal anatomy and correct equipment proportions. Dignified contemporary clothing, natural concentration, professional skills and human capital investment, no posed smiles or stereotypes.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-d21314d7-c726-41ad-862c-3c8b64df6b53.png",
"original": "outputs/investment-context-20261023/originals/educacao-formacao.png",
"originalSha256": "e59ec599e8c64ab84195d0cb37871d3102df178a13275806ca3b7f98e6bfa895",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-context/educacao-formacao.webp",
"width": 960,
"height": 540,
"bytes": 68224,
"sha256": "e3e5dbd292cac555a45bcbd94a0fa6b60e6b710debc0c827c68603f40bfb8a28"
},
{
"file": "public/images/investment-context/educacao-formacao-480.webp",
"width": 480,
"height": 270,
"bytes": 27384,
"sha256": "d3b743fbba7a8cb40e45999ee8c345cbfc0332cf85f5c5c2d6a5cf1240a395e4"
},
{
"file": "public/images/investment-context/educacao-formacao-fallback.webp",
"width": 240,
"height": 135,
"bytes": 7156,
"sha256": "0b2a12898043c57cf4354b54ebcba8adec8d41b3886bc574584ccc12af7b1e4c"
}
]
},
{
"key": "documentos-formularios",
"prompt": "Use case: photorealistic-natural. Asset type: an individual navigation card photograph for a professional investment portal. Generate ONE clean, realistic editorial photograph in horizontal 16:9 composition. Natural daylight, realistic restrained colours, excellent photographic detail, balanced uncluttered framing, one clear subject readable at a small thumbnail size. Contemporary setting plausible in Mozambique in southern Africa. This is a generic illustrative scene, NOT a depiction or claim about any named real facility, project, landmark or institution. No text, lettering, titles, captions, logos, brands, watermarks, flags, official emblems, maps, diagrams, graphical overlays, UI elements, collages, artificial colour filters, futuristic architecture or advertising poster treatment. Realistic people, hands, equipment and perspective; dignified candid activity instead of posed stock photography. Subject: a close-medium editorial photograph of a tidy modern administrative office desk where a Black southern African professional in a plain light blue shirt is naturally reviewing organised business documentation. Frame hands, desk and partial torso, no prominent portrait. Neat neutral folders, a few sheets with only faint out-of-focus non-readable printing, a pen and an unbranded laptop angled away with no visible UI. Soft window light, white and pale neutral surfaces with subtle blue accents. Absolutely no readable personal data, invented official seals or highlighted fake text; realistic fingers and documents, calm minimal composition.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f1c8e22e-6885-4578-ac7d-02692c5cc071.png",
"original": "outputs/investment-context-20261023/originals/documentos-formularios.png",
"originalSha256": "cfbd38a9cc9b9acf35f38220f5bd9c1ab1feafe411bc1d4bbdb3658de557e052",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-context/documentos-formularios.webp",
"width": 960,
"height": 540,
"bytes": 43002,
"sha256": "70ddea3256e6a8d981b1fae5ee7b4de6c910d757137b79a59306c268c278402f"
},
{
"file": "public/images/investment-context/documentos-formularios-480.webp",
"width": 480,
"height": 270,
"bytes": 17996,
"sha256": "436865c948acbb7b6e9e482bef8f5bd626c6960f2f155608e40dfcd0e76b84cb"
},
{
"file": "public/images/investment-context/documentos-formularios-fallback.webp",
"width": 240,
"height": 135,
"bytes": 4958,
"sha256": "fad6a6ab825a82ec2541d2e0c6de9d85486e4bbca159b912f87bec9cc0aeeba5"
}
]
}
],
"primaryImagesBytesTotal": 390320,
"responsive480ImagesBytesTotal": 143146,
"implementation": {
"gridColumns": [
1,
2,
4
],
"aspectRatio": "16:9",
"responsiveImages": true,
"lazyLoading": true,
"altLanguage": "pt",
"fallback": "Derivado pequeno da própria imagem; se também falhar, estado acessível Imagem indisponível, com dimensão reservada e link mantido.",
"globalCSSChanged": false,
"businessLogicChanged": false,
"databaseChanged": false,
"heroChanged": false
},
"visualReview": "Cinco composições individuais distintas, cores naturais e luz editorial; sem títulos/colagens ou mapas. A composição de Corredores foi revista para remover o camião com marca. Capturas em computador e telemóvel inspeccionadas.",
"verification": {
"browser": {
"results": [
{
"width": 320,
"dpr": 1,
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 320,
"height": 180
}
],
"status": "passed"
},
{
"width": 390,
"dpr": 2,
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
"width": 389,
"height": 219
}
],
"status": "passed"
},
{
"width": 768,
"dpr": 1,
"columns": 2,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 384,
"height": 216
}
],
"status": "passed"
},
{
"width": 1024,
"dpr": 1,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 256,
"height": 144
}
],
"status": "passed"
},
{
"width": 1440,
"dpr": 2,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
"width": 266,
"height": 149
}
],
"status": "passed"
},
{
"width": 1920,
"dpr": 1,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 266,
"height": 149
}
],
"status": "passed"
},
{
"fail": "primary",
"status": "passed"
},
{
"fail": "all",
"status": "passed"
}
],
"errors": [],
"navigation": [
"/clusters",
"/corredores",
"/projectos-ancora",
"/educacao",
"/documentos"
],
"databaseAccess": false
},
"script": "tools/portal-context-cards-qa.mjs",
"screenshots": [
"outputs/investment-context-20261023/cards-1024-dpr1.png",
"outputs/investment-context-20261023/cards-1440-dpr2.png",
"outputs/investment-context-20261023/cards-1920-dpr1.png",
"outputs/investment-context-20261023/cards-320-dpr1.png",
"outputs/investment-context-20261023/cards-390-dpr2.png",
"outputs/investment-context-20261023/cards-768-dpr1.png",
"outputs/investment-context-20261023/fallback-all.png",
"outputs/investment-context-20261023/fallback-primary.png"
],
"types": {
"command": "npm run types:check",
"exitCode": 0
},
"lint": {
"files": [
"resources/js/components/investment-context-cards.tsx",
"resources/js/pages/PortalInvestidor.tsx",
"tools/portal-context-cards-qa.mjs"
],
"exitCode": 0
},
"build": {
"command": "npm run build",
"exitCode": 0,
"pageAsset": "assets/PortalInvestidor-fh5VZanx.js",
"log": "outputs/investment-context-20261023/build-local.log"
}
},
"scope": "Aplicado no código e pacote locais. API e navegação simuladas na verificação visual; sem dados activos ou publicação externa.",
"matrixReferences": [
"MASTER-efd550fd-0104",
"MASTER-55050f60-0001"
],
"acceptanceLimit": "Prova restrita à apresentação e navegação dos cinco cartões; não encerra critérios funcionais mais amplos da matriz.",
"layoutRevision": {
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: título a vermelho e menos espaço em branco dentro dos cartões.",
"changes": "Título no vermelho #ed1c24; removidas alturas mínimas de título e descrição; preenchimento 20→16 px, intervalo 8→6 px e entrelinhas 28/24→24/20 px. Mantidos imagens, conteúdos, links e alinhamento por linha da grelha.",
"browser": {
"results": [
{
"width": 320,
"dpr": 1,
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 320,
"height": 180
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 320,
"height": 180
}
],
"status": "passed"
},
{
"width": 390,
"dpr": 2,
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
"width": 389,
"height": 219
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
"width": 389,
"height": 219
}
],
"status": "passed"
},
{
"width": 768,
"dpr": 1,
"columns": 2,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 384,
"height": 216
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 384,
"height": 216
}
],
"status": "passed"
},
{
"width": 1024,
"dpr": 1,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 256,
"height": 144
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 256,
"height": 144
}
],
"status": "passed"
},
{
"width": 1440,
"dpr": 2,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios.webp",
"width": 266,
"height": 149
}
],
"status": "passed"
},
{
"width": 1920,
"dpr": 1,
"columns": 4,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-context/clusters-agroindustria-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/corredores-logisticos-v2-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/projectos-ancora-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/educacao-formacao-480.webp",
"width": 266,
"height": 149
},
{
"src": "http://127.0.0.1:5174/images/investment-context/documentos-formularios-480.webp",
"width": 266,
"height": 149
}
],
"status": "passed"
},
{
"fail": "primary",
"status": "passed"
},
{
"fail": "all",
"status": "passed"
}
],
"errors": [],
"navigation": [
"/clusters",
"/corredores",
"/projectos-ancora",
"/educacao",
"/documentos"
],
"databaseAccess": false
},
"lintExitCode": 0,
"buildExitCode": 0,
"pageAsset": "assets/PortalInvestidor-CYCMhSzW.js",
"screenshots": [
"outputs/investment-context-compact-20261023/cards-1024-dpr1.png",
"outputs/investment-context-compact-20261023/cards-1440-dpr2.png",
"outputs/investment-context-compact-20261023/cards-1920-dpr1.png",
"outputs/investment-context-compact-20261023/cards-320-dpr1.png",
"outputs/investment-context-compact-20261023/cards-390-dpr2.png",
"outputs/investment-context-compact-20261023/cards-768-dpr1.png"
],
"databaseChanged": false
}
}
@@ -0,0 +1,355 @@
{
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: adaptar hero, imagens e pesquisa da referência, preservando a informação e os comportamentos equivalentes do V2.",
"sourceProject": "D:/Projects/Tempo_Investidor/investidor",
"targetProject": "D:/Projects/Tempo_Investidor/Investidor-V2",
"planAndAnalysis": "docs/investmoz/public-portal-sections.md#adequação-do-hero--23102026",
"implementation": [
"resources/js/components/portal-hero.tsx",
"resources/js/pages/PortalInvestidor.tsx"
],
"assets": [
{
"source": "D:\\Projects\\Tempo_Investidor\\investidor\\public\\images\\hero\\hero-1.png",
"original": "public\\images\\hero\\hero-1.png",
"sha256": "3c9246169025b9b1b4d1d10fb51c1f644123984bde56c6c2d9bf869d851024fc",
"bytes": 2315768,
"webpBytes": 94012,
"mobileBytes": 38804
},
{
"source": "D:\\Projects\\Tempo_Investidor\\investidor\\public\\images\\hero\\hero-3.jpg",
"original": "public\\images\\hero\\hero-3.jpg",
"sha256": "91377213b6286aca7ee4af90860a7dc7a75fa08a165ef7b3192081db43f92dff",
"bytes": 9875137,
"webpBytes": 140342,
"mobileBytes": 32998
}
],
"desktopBytesTotal": 234354,
"mobileBytesTotal": 71802,
"originalBytesTotal": 12190905,
"verification": {
"browser": {
"results": [
{
"width": 320,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 390,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 768,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1920,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "no-preference",
"status": "passed"
}
],
"errors": [],
"requests": [
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=",
"/oportunidades",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
],
"databaseAccess": false
},
"browserScript": "tools/portal-hero-qa.mjs",
"screenshots": [
"outputs/portal-hero-20261023/after-1440-no-preference.png",
"outputs/portal-hero-20261023/after-1440-reduce.png",
"outputs/portal-hero-20261023/after-1440-second-image.png",
"outputs/portal-hero-20261023/after-1920-reduce.png",
"outputs/portal-hero-20261023/after-320-reduce.png",
"outputs/portal-hero-20261023/after-390-reduce.png",
"outputs/portal-hero-20261023/after-768-reduce.png",
"outputs/portal-hero-20261023/before-1440-reduce.png",
"outputs/portal-hero-20261023/before-390-reduce.png"
],
"additionalCases": [
"pesquisa por Enter e botão",
"ordem do teclado: campo → botão",
"query acentuada e pontuação preservadas",
"remoção de espaços exteriores",
"pesquisa vazia abre catálogo",
"CTA abre oportunidades",
"API indisponível mantém indicadores indeterminados",
"contagens zero preservadas",
"duas imagens carregadas",
"selecção manual",
"pausa e retoma explícitas",
"foco pausa rotação",
"rotação automática após perda de foco",
"movimento reduzido",
"entrada documental e cabeçalho preservados"
],
"existingFrontendTests": {
"command": "node --test tests/frontend/portal-api.test.mjs tests/frontend/portal-navigation.test.mjs",
"passed": 20
},
"typecheck": {
"command": "npm run types:check",
"exitCode": 0
},
"lint": {
"files": [
"resources/js/components/portal-hero.tsx",
"resources/js/pages/PortalInvestidor.tsx",
"tools/portal-hero-qa.mjs"
],
"exitCode": 0
},
"qaBuild": {
"command": "INVESTMOZ_QA_BUILD=1 npm run build",
"exitCode": 0
},
"localBuild": {
"command": "npm run build",
"exitCode": 0,
"pageAsset": "assets/PortalInvestidor-MsUeKoJQ.js"
}
},
"matrix": {
"file": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
"referencesAdded": [
"MASTER-9dee08d0-0016",
"MASTER-efd550fd-0104",
"MASTER-55050f60-0001"
],
"rowCount": 3422,
"criteriaAndStatesPreserved": true
},
"scope": {
"appliedToLocalCodeAndBuild": true,
"activeDatabaseChanged": false,
"migrationsRun": false,
"externalDeploymentPerformed": false,
"backendSearchReimplemented": false
},
"limits": "Verificação visual com dados sintéticos; pesquisa continua limitada ao catálogo de oportunidades. Os critérios mais amplos da matriz permanecem no estado anterior.",
"latestRevision": {
"id": "hero-no-controls-20261023",
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: remover do hero as duas bolinhas e o botão de pausa das imagens.",
"implementation": "Removidos grupo de controlos, ícones Pause/Play e estado de pausa manual. Preservadas troca automática, escrita dinâmica, pesquisa e preferência de movimento reduzido.",
"browser": {
"results": [
{
"width": 320,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 390,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 768,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1920,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 390,
"reducedMotion": "no-preference",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "no-preference",
"status": "passed"
}
],
"errors": [],
"requests": [
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=",
"/oportunidades",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=",
"/oportunidades",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
],
"databaseAccess": false
},
"screenshots": [
"outputs/portal-hero-no-controls-20261023/after-1440-no-preference.png",
"outputs/portal-hero-no-controls-20261023/after-1440-reduce.png",
"outputs/portal-hero-no-controls-20261023/after-1920-reduce.png",
"outputs/portal-hero-no-controls-20261023/after-320-reduce.png",
"outputs/portal-hero-no-controls-20261023/after-390-no-preference.png",
"outputs/portal-hero-no-controls-20261023/after-390-reduce.png",
"outputs/portal-hero-no-controls-20261023/after-768-reduce.png",
"outputs/portal-hero-no-controls-20261023/second-image-1440.png",
"outputs/portal-hero-no-controls-20261023/second-image-390.png",
"outputs/portal-hero-no-controls-20261023/typed-1440.png",
"outputs/portal-hero-no-controls-20261023/typed-390.png"
],
"checks": {
"lintExitCode": 0,
"buildExitCode": 0,
"pageAsset": "assets/PortalInvestidor-T0B6aoV_.js"
},
"scope": {
"appliedLocally": true,
"databaseAccess": false,
"externalDeployment": false
}
},
"revisionHistory": [
{
"id": "hero-typing-20261023",
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: título e frase dinâmicos como na referência; remover Termos de uso do hero.",
"plan": [
"Analisar a sequência de escrita da referência.",
"Animar o título e depois a frase, mantendo os textos do V2 e reservando a dimensão final.",
"Reiniciar a escrita na troca de imagem e integrar pausa/retoma e preferência de movimento reduzido.",
"Remover o link e o ícone jurídico do hero.",
"Demonstrar comportamento em computador e telemóvel e compilar o pacote local."
],
"files": [
"resources/js/components/portal-hero.tsx",
"resources/css/app.css",
"tools/portal-hero-qa.mjs"
],
"browser": {
"results": [
{
"width": 320,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 390,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 768,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 1920,
"reducedMotion": "reduce",
"status": "passed"
},
{
"width": 390,
"reducedMotion": "no-preference",
"status": "passed"
},
{
"width": 1440,
"reducedMotion": "no-preference",
"status": "passed"
}
],
"errors": [],
"requests": [
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=",
"/oportunidades",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique",
"/oportunidades?q=",
"/oportunidades",
"/oportunidades?q=energia%20%26%20Nampula%20%2F%20Mo%C3%A7ambique"
],
"databaseAccess": false
},
"screenshots": [
"outputs/portal-hero-typing-20261023/after-1440-no-preference.png",
"outputs/portal-hero-typing-20261023/after-1440-reduce.png",
"outputs/portal-hero-typing-20261023/after-1440-second-image.png",
"outputs/portal-hero-typing-20261023/after-1920-reduce.png",
"outputs/portal-hero-typing-20261023/after-320-reduce.png",
"outputs/portal-hero-typing-20261023/after-390-no-preference.png",
"outputs/portal-hero-typing-20261023/after-390-reduce.png",
"outputs/portal-hero-typing-20261023/after-768-reduce.png",
"outputs/portal-hero-typing-20261023/typed-1440.png",
"outputs/portal-hero-typing-20261023/typed-390.png",
"outputs/portal-hero-typing-20261023/typing-1440.png",
"outputs/portal-hero-typing-20261023/typing-390.png"
],
"demonstrated": [
"escrita progressiva",
"título antes da frase",
"texto completo no modo de movimento reduzido",
"nome acessível completo durante a escrita",
"pesquisa sem deslocamento durante a escrita",
"reinício na selecção manual e automática de imagem",
"pausa e retoma da escrita",
"cursor com pausa",
"Termos de uso ausente do hero",
"pesquisa, teclado, CTA, API indisponível e contagens zero preservados"
],
"checks": {
"types": {
"command": "npm run types:check",
"exitCode": 0
},
"lint": {
"command": "vp lint resources/js/components/portal-hero.tsx tools/portal-hero-qa.mjs",
"exitCode": 0
},
"build": {
"command": "npm run build",
"exitCode": 0,
"pageAsset": "assets/PortalInvestidor-BbVHeTeI.js",
"log": "outputs/portal-hero-typing-20261023/build-local.log"
}
},
"scope": {
"appliedLocally": true,
"databaseAccess": false,
"externalDeployment": false
}
}
]
}
+88
View File
@@ -48,3 +48,91 @@ Edge headless: 11 páginas públicas/autenticação verificadas a 390×844, com
Página inicial também verificada a 1440×1000. Cenário fiscal exercitado no navegador: base 1.000 × taxa 10% × (1 − redução 50%) = 50. Resultados em `public-portal-browser-validation.json`.
Antes da migration local foi criado e restaurado o backup de 74 tabelas em `storage/app/private/portal-services-backup-20260927-090641/`. O backup completo anterior permanece em `C:/laragon/backups/portal-investidor-20260927-101224/` e representa o estado anterior a esta entrega.
## Adequação do hero — 23/10/2026
Origem: pedido expresso da Tempo nesta sessão para aproveitar o hero, as imagens e a barra de pesquisa de `D:/Projects/Tempo_Investidor/investidor`, usando a informação existente no V2. Aplicam-se as orientações permanentes do `AGENTS.md`, incluindo a distinção entre decisão da Tempo e escolha técnica, a preservação de dados e a separação de QA e activo.
Análise prévia: a página de entrada real do V2 é `PortalInvestidor.tsx`, servida por `PortalServicesController::home`. A referência é `domains/investor-portal/components/homepage/homepage-hero.tsx`, montada em `InstitutionalHomepage`. A referência usa Inertia 2, Framer Motion, tradução e navegação diferentes; o V2 usa Inertia 3 e pesquisa de oportunidades. Copiar essas dependências e `/pesquisa` ou `/aviso-legal` directamente criaria incompatibilidades e destinos inexistentes.
Plano executado:
1. Registar a apresentação anterior em computador e telemóvel, com os componentes reais e dados sintéticos.
2. Copiar os dois originais efectivamente usados pela referência (`hero-1.png` e `hero-3.jpg`), conferir SHA-256 e gerar derivados WebP para computador e telemóvel. `hero-2.png` não faz parte da rotação da referência.
3. Extrair `components/portal-hero.tsx` e aplicar fotografias em rotação, texto centrado e pesquisa branca com botão vermelho. Preservar os textos do V2, contagens da API, sectores, documentos, apoio, cabeçalho e rodapé.
4. Manter a pesquisa de oportunidades em `/oportunidades?q=`, incluindo pesquisa vazia e caracteres acentuados. Conservar “Consultar oportunidades” e “Planear finanças”. A primeira iteração acrescentou o acesso jurídico `/termos-de-uso` ao hero; foi removido na revisão expressamente pedida pela Tempo abaixo.
5. Verificar apresentação, teclado, imagens, rotação, pausa, movimento reduzido, falha da API, contagens zero, TypeScript, lint e compilação. Compilar primeiro o pacote QA e depois os assets locais em `public/build`.
Escolhas técnicas desta adaptação: React e transições CSS existentes, sem novas dependências; rotação de oito segundos, pausa por controlo explícito, foco, ponteiro ou separador oculto; apresentação estática quando o utilizador pede movimento reduzido. A primeira iteração apresentou o texto estático; a revisão abaixo introduz a escrita dinâmica pedida pela Tempo. O campo indica pesquisa de oportunidades e conserva exemplos que o catálogo suporta; esta alteração não implementa uma pesquisa global de legislação/licenciamento.
Evidência reproduzível: `tools/portal-hero-qa.mjs`, com Vite local e Playwright configurável por `HERO_QA_PLAYWRIGHT`/`HERO_QA_CHROMIUM`. Fixtures e capturas em `outputs/portal-hero-20261023/`; síntese persistente em `public-portal-hero-validation.json`. Os testes de interface interceptam a API e a navegação Inertia, sem acesso a Laravel, base de dados ou serviços externos. A compilação local aplica os assets desta pasta; não representa publicação noutro servidor.
Referências na matriz existente: `MASTER-efd550fd-0104` (composição da página), `MASTER-9dee08d0-0016` (contexto da pesquisa) e `MASTER-55050f60-0001` (entrada documental preservada). Esta prova cobre a adaptação visual e os percursos exercitados; não encerra os critérios funcionais mais amplos dessas linhas.
### Revisão: escrita dinâmica e remoção do acesso jurídico no hero
Decisão expressa da Tempo na continuação da sessão de 23/10/2026: animar “INVISTA EM MOÇAMBIQUE” e “Transforme informação em decisões e ideias em negócios reais.” como na página de referência, e remover “Termos de uso” do hero. A anterior apresentação estática e a inclusão desse link eram escolhas de implementação, agora substituídas pelo pedido do proprietário.
Análise e plano executado: reproduzir a sequência da referência (espera inicial de 250 ms, título a 50 ms por carácter, intervalo de 150 ms e frase a 28 ms por carácter); reiniciar ao trocar de imagem; acrescentar o cursor intermitente; reservar a dimensão final de ambas as linhas para manter a pesquisa imóvel; remover o link e o ícone jurídico do hero; verificar em computador e telemóvel e compilar o pacote local.
Implementação em `portal-hero.tsx`, com React existente e CSS do cursor em `app.css`, sem dependências novas. A pausa explícita suspende também a escrita, retomando do mesmo ponto. Quando o separador fica oculto a escrita pára; a preferência de movimento reduzido apresenta os dois textos completos. Leitores de ecrã recebem o texto completo uma única vez, independentemente do número de caracteres visíveis. Os temporizadores são limpos ao mudar de imagem ou desmontar o componente.
O script `tools/portal-hero-qa.mjs` demonstra a escrita progressiva, a ordem título→frase, o reinício manual/automático, a pausa/retoma, a posição estável da pesquisa, o modo sem movimento e a ausência de “Termos de uso” no hero. As capturas e resultados desta revisão ficam em `outputs/portal-hero-typing-20261023/`, preservando a evidência anterior. Síntese e compilação final registadas na revisão de `public-portal-hero-validation.json`; referência acrescentada à linha existente `MASTER-efd550fd-0104`, sem encerrar o seu critério mais amplo.
### Revisão: remoção dos controlos visíveis das imagens
Pedido expresso da Tempo na continuação de 23/10/2026: remover do hero o grupo com o botão de pausa e as duas bolinhas de selecção de imagem. Removidos o grupo, os ícones e o estado de pausa manual. A troca automática de imagens e a escrita dinâmica permanecem; conservam-se a pausa da rotação por foco/ponteiro, a suspensão em separador oculto e a preferência de movimento reduzido. Esta decisão substitui os controlos visíveis introduzidos como escolha de implementação nas revisões anteriores.
O script existente de interface foi adaptado para demonstrar a ausência dos controlos e a continuação da rotação automática, escrita e pesquisa, com capturas em `outputs/portal-hero-no-controls-20261023/`. Evidência e pacote local registados em `public-portal-hero-validation.json`, mantendo o histórico das revisões e os estados dos requisitos da matriz.
## Fotografias nas etapas de Como Investir — 23/10/2026
Origem: pedido expresso da Tempo para analisar e ilustrar exclusivamente os cartões “1. Explorar”, “2. Preparar” e “3. Acompanhar”. Aplicam-se as orientações permanentes de `AGENTS.md` de 12/10 e 22/10/2026; componentes e optimização de recursos são escolhas técnicas, sem novas regras operacionais.
Análise e plano: os cartões existentes são âncoras estáticas na home React/Inertia, numa grelha de três colunas a partir de 768 px. Explorar liga a `/oportunidades` e representa a identificação de sector, território e oportunidade; Preparar liga a `/como-investir` e representa percurso/documentação; Acompanhar liga a `/balcao-virtual` e representa envio/consulta de pedido e protocolo. Os recursos locais existentes ilustram outras secções; preparar três fotografias distintas para evitar repetição. Gerar cenas editoriais plausíveis com `imagegen`, rever composição, guardar originais e versões WebP locais, integrar imagens 16:9 com dimensões reservadas e carregamento diferido, manter textos/destinos/título vermelho, verificar comportamento responsivo, navegação e falhas de imagem, compilar. Âmbito exclusivamente visual, sem alterações em dados, autenticação, apoio ou rodapé. Referência documental na linha existente `MASTER-efd550fd-0104`, sem criar outra matriz nem encerrar o critério mais amplo.
Implementação: `components/investment-steps.tsx` reúne os três cartões, preservando títulos, descrições e âncoras. Fotografias individuais geradas com a ferramenta integrada `image_gen`, sem local ou projecto real atribuído: avaliação de oportunidade num parque empresarial genérico, preparação documental numa reunião e consulta de pedido no computador. Originais em `outputs/investment-steps-20261023/originals/`; recursos locais em `public/images/investment-steps/` com WebP 960×540, 480×270 e alternativa própria 240×135. Os cartões usam `srcset`, `sizes`, `loading="lazy"`, dimensões reservadas, corte 16:9, alt em português, foco visível e texto compacto com 16 px de preenchimento. Se também falhar a alternativa pequena, o cartão mantém a área reservada, descrição acessível e destino utilizável.
Verificação desta secção: componentes reais com API sintética e sem base de dados, seis larguras de 320 a 1920 px, duas densidades de ecrã, três imagens distintas, texto/destinos conservados, navegação por teclado, ausência de transbordo e deslocamento no carregamento, falhas dos recursos principais e de todos os recursos. Oito cenários sem erros JavaScript. Fotografias, prompts, hashes, capturas, relatório de browser e compilação documentados em `public-portal-steps-images.json` e `outputs/investment-steps-20261023/`. Ajuste restrito a esta secção; apoio, acesso à gestão e rodapé mantidos.
## Fotografias nos cartões de contexto do investimento — 23/10/2026
Origem: pedido expresso da Tempo para aplicar o anexo “Melhorar as imagens e a identidade visual do Portal do Investidor de Moçambique” exclusivamente à secção “Encontre o contexto do seu investimento”. O anexo define estética fotográfica editorial, imagens individuais contextualizadas, recursos locais, acessibilidade, desempenho e preservação dos cinco destinos e conteúdos. Aplicam-se as orientações permanentes de `AGENTS.md` de 12/10 e 22/10/2026; os pormenores de implementação abaixo são escolhas técnicas.
Análise prévia: Laravel/Inertia 3 e React, com Tailwind existente; os cinco cartões estavam embutidos em `pages/PortalInvestidor.tsx`. `EducationalArticleCard` destina-se a artigos com acções e metadados diferentes, pelo que não foi alterado. A secção foi extraída para `components/investment-context-cards.tsx`, mantendo o título, descrições, âncoras e grelha de uma/duas/quatro colunas. Não foram alteradas as páginas de destino, dados, APIs ou estilos globais.
Foram geradas cinco imagens distintas com a ferramenta integrada `image_gen`, segundo a skill `imagegen`: agro-indústria, terminal logístico, acompanhamento de instalação solar, formação técnica e análise documental. São imagens fotográficas ilustrativas geradas, sem localização ou instituição real atribuída, e não fotografias documentais de projectos cadastrados. A imagem do terminal foi revista para remover o camião com marca visível. Não foram usadas fotografias externas nem URLs de serviços de imagens. Originais seleccionados, prompts e hashes estão registados no manifesto `public-portal-context-images.json`; os originais foram preservados dentro do workspace.
Recursos finais em `public/images/investment-context/`: `clusters-agroindustria`, `corredores-logisticos-v2`, `projectos-ancora`, `educacao-formacao` e `documentos-formularios`, em WebP 960×540, com variantes 480×270 e alternativas 240×135. As imagens mantêm 16:9, `object-cover`, textos alternativos em português, dimensões reservadas, `srcset`, `sizes`, `loading="lazy"` e descodificação assíncrona. Cada cartão conserva o seu título e descrição; o foco usa o azul e os tons neutros pedidos no anexo.
Se um recurso principal falhar, o cartão usa a sua própria variante pequena, evitando repetir a fotografia de outra secção. Se também essa falhar, apresenta “Imagem indisponível” no mesmo espaço reservado, com descrição acessível e link operacional. Este é apenas um estado de erro; os cinco cartões usam imagens fotográficas na apresentação normal.
Verificação reproduzível: `tools/portal-context-cards-qa.mjs`, com o portal e cabeçalho reais, API sintética e acesso exclusivo aos recursos locais. Exercita seis larguras de ecrã, ecrãs de alta densidade, os cinco destinos por teclado, imagens carregadas e distintas, alinhamento, dimensões, ausência de deslocamento no carregamento e dois cenários de falha. Capturas e resultados em `outputs/investment-context-20261023/`; síntese, fontes/prompts e compilação em `public-portal-context-images.json`. Referências acrescentadas às linhas existentes `MASTER-efd550fd-0104` e `MASTER-55050f60-0001`, sem encerrar os respectivos critérios mais amplos.
Revisão expressa da Tempo de 23/10/2026: título da secção no vermelho da identidade existente (`#ed1c24`) e cartões mais compactos. Removidas as alturas mínimas de 56 e 72 px do título/descrição; reduzidos o preenchimento de 20 para 16 px, o intervalo de 8 para 6 px e as entrelinhas. As alturas passam a acompanhar o conteúdo, com alinhamento por linha da grelha, conservando imagens, textos e destinos. A reserva das dimensões das imagens mantém-se. Evidência desta revisão em `outputs/investment-context-compact-20261023/` e no campo `layoutRevision` do manifesto existente.
## Sectores da home e catálogo completo — 23/10/2026
Origem: pedido expresso da Tempo para adaptar a estrutura de “Áreas de Investimento” do projecto `D:/Projects/Tempo_Investidor/tempo-dashboard`, incluindo cartões, botão para mais sectores e respectiva página, e uniformizar a vermelho os títulos das secções da home. Aplicadas as orientações permanentes `AGENTS.md` de 12/10 e 22/10/2026: as decisões de produto são da Tempo; as escolhas técnicas abaixo não criam requisitos organizacionais.
Análise prévia: a referência usa `components/investor-portal/homepage/homepage-sectors.tsx` e `pages/investor-portal/sectors/index.tsx`. Mostra cinco cartões com fotografia e título sobreposto, resumo, “Saiba mais” e ligação central arredondada para o catálogo. O V2 já dispunha de `/sectores`, fichas `/sectores/{slug}`, especialização Fintech e dados de `SectorOrganization`. Foram reutilizados esses contratos e percursos, sem importar Framer Motion, traduções ou rotas exclusivas da referência.
Implementação: `components/public-sector-cards.tsx` partilha os cartões entre a home e o catálogo. A home mostra os primeiros cinco sectores, na ordem recebida da API, com “Ler mais” para a ficha existente e “Ver mais sectores” para `/sectores`. A página existente apresenta todos os sectores publicados, na mesma estrutura visual, e “Voltar ao início”. Mantêm-se nomes, descrições, contagens, valores zero, especializações e URLs canónicos recebidos. Descrições ausentes não são preenchidas com dados da referência. Fintech permanece ligado à ficha própria, sem âncoras HTML aninhadas.
Foram inventariados os onze JPG disponíveis em `storage/app/public/investor-portal/sectors` na referência, copiados os originais para evidência e preparados derivados locais WebP 960×540/480×270 em `public/images/sectors/`. O mapeamento visual por slug é de apresentação e não altera a taxonomia. Para Imobiliário e sectores sem imagem específica usa-se a vista empresarial genérica disponível na referência; para Bens de Consumo e Franquias usa-se a imagem de actividade industrial. A variante genérica também serve como alternativa em caso de erro; se ambas falharem, o título e as ligações continuam visíveis num fundo neutro. Não se atribui aos ficheiros uma origem oficial, licença externa ou localização documental não comprovada.
Os seis títulos de secção da home — contexto do investimento, sectores, Como Investir, oportunidades em destaque, documentos essenciais e apoio — usam `HOME_SECTION_HEADING` em `lib/portal-presentation.ts`: vermelho `#ed1c24`, 24 px, entrelinha de 32 px e peso 700. O cartão de Apoio passou a superfície clara para manter contraste com o título vermelho. O título principal animado do hero mantém a sua hierarquia própria. `PORTAL_MORE_LINK` centraliza o botão arredondado, contorno vermelho e estado de foco, seguindo a referência.
Verificação: `tools/portal-home-sectors-qa.mjs` carrega os componentes reais com dez sectores sintéticos, descrições nulas e especialização Fintech, sem base de dados. Exercita seis larguras, prévia de cinco/cartálogo de dez, estilos dos seis títulos, percurso por teclado, dez fichas, Fintech, regresso à home, catálogo vazio e dois cenários de indisponibilidade de imagens. Capturas, inventário, hashes e resultados em `outputs/home-sectors-20261023/`; resumo persistente em `public-portal-sectors-validation.json`. Referências na matriz existente: `MASTER-efd550fd-0104` e `MASTER-9f5719c8-0077`. Esta adaptação não encerra critérios mais amplos nem altera dados, permissões, regras operacionais ou publicação noutro servidor.
Pedido expresso da Tempo, 23/10/2026 — botão “Contactar Apoio ao Investidor”: em `layouts/portal-navigation.tsx`, a faixa de largura total foi substituída por um botão centrado, com largura ajustada ao texto, altura mínima de 44 px, preenchimento reduzido, sombra discreta e vermelho `#ed1c24`, conservando `/apoio` e o foco visível. Verificado a 320, 390 e 1440 px: cerca de 223×44 px, sem transbordo ou erros JavaScript. Evidência em `outputs/support-button-20261023/qa-results.json` e capturas; compilação local no mesmo directório. Ajuste de apresentação por decisão do proprietário, sem nova regra operacional nem alteração de dados.
Revisão anterior de 23/10/2026, superada pela correcção de âmbito abaixo: o Codex interpretou o pedido de abreviação como aplicável à zona inferior inteira e alterou indevidamente o botão de apoio e o rodapé partilhado. Essa interpretação não foi uma decisão da Tempo. A revisão removeu o bloco interno, abreviou o botão para “Apoio” e recolheu contactos/ligações em “Informações” num rodapé claro; estas alterações fora do bloco indicado foram revertidas após a clarificação expressa do proprietário. O registo desta revisão é histórico e não descreve a apresentação actual.
Evidência histórica da revisão superada: `outputs/footer-compact-20261023/qa-results.json`, capturas e `check.mjs`. Não constitui aceitação do âmbito pelo proprietário.
Correcção expressa da Tempo, 23/10/2026: o pedido abrange exclusivamente o bloco “INVESTE Moçambique / Portal do Investidor — conheça as oportunidades… / Acesso à gestão”. Em `pages/PortalInvestidor.tsx`, esse bloco fica abreviado num ícone de acesso à gestão, com nome acessível e descrição “Acesso à gestão”, destino `/acesso-gestao`, área de interacção de 44 px e fundo branco, sem o título e parágrafo anteriores. O botão de apoio foi restaurado exactamente à apresentação anterior à abreviação: “Contactar Apoio ao Investidor”, compacto, vermelho e sem ícone. `components/tempo-footer.tsx` e os estilos `.tempo-footer` foram integralmente restaurados ao estado anterior, incluindo contactos e ligações visíveis e o fundo original do rodapé geral. Não se altera autenticação nem dados. Esta correcção substitui a interpretação indevida acima e mantém as restantes alterações previamente pedidas.
Evidência da correcção: `outputs/footer-scope-correction-20261023/check.mjs`, `qa-results.json`, capturas e `build-local.log`; componentes reais com API sintética a 320, 390 e 1440 px, ícone de gestão focável, bloco específico claro sem parágrafo, botão de apoio completo/vermelho, rodapé geral restaurado, contactos e ligações legais visíveis, sem transbordo ou erros JavaScript. Lint dos componentes alterados e compilação local. Referência na linha existente `MASTER-efd550fd-0104`, sem encerramento do critério amplo nem alteração dos seus estados.
Continuação expressa da Tempo, 23/10/2026: acrescentado o título visível “Acesso à gestão” ao lado do ícone nesse mesmo bloco, para tornar o destino compreensível. O acesso mantém-se compacto, vermelho e com fundo claro, conservando `/acesso-gestao`. Ajuste restrito a essa ligação; apoio e rodapé geral conservam a apresentação restaurada. Referência: `MASTER-efd550fd-0104` e a evidência da revisão anterior acima. Verificado a 320, 390 e 1440 px, sem transbordo ou erros JavaScript; lint e compilação aprovados. Evidência em `outputs/management-label-20261023/qa-results.json`, capturas e `build-local.log`. No pedido seguinte, a Tempo determinou o alinhamento desse acesso à esquerda; aplicado `justify-start` apenas no seu contentor.
@@ -0,0 +1,748 @@
{
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: adaptar Áreas de Investimento do tempo-dashboard aos sectores da home e ao catálogo completo, copiar a estrutura dos botões e uniformizar a vermelho os títulos de secção da home.",
"reference": {
"project": "D:/Projects/Tempo_Investidor/tempo-dashboard",
"files": [
"resources/js/components/investor-portal/homepage/homepage-sectors.tsx",
"resources/js/pages/investor-portal/sectors/index.tsx"
],
"structure": "Cinco cartões com fotografia, título sobreposto, resumo e acção de leitura; botão central arredondado/contorno vermelho para o catálogo."
},
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#sectores-da-home-e-catálogo-completo--23102026",
"filesChanged": [
"resources/js/components/public-sector-cards.tsx",
"resources/js/lib/portal-presentation.ts",
"resources/js/components/investment-context-cards.tsx",
"resources/js/lib/portal-api.ts",
"resources/js/pages/PortalInvestidor.tsx",
"resources/js/pages/portal/services.tsx",
"tools/portal-home-sectors-qa.mjs",
"docs/investmoz/public-portal-sections.md",
"docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv"
],
"behaviour": {
"homePreview": 5,
"previewOrder": "Ordem recebida da API, sem novos filtros de publicação.",
"readMore": "Ler mais, para a URL recebida ou ficha existente /sectores/{slug}.",
"moreSectors": "Ver mais sectores, botão arredondado para /sectores.",
"catalogue": "Todos os sectores publicados devolvidos pelo cadastro existente; Voltar ao início para /.",
"specializations": "Fintech conservado como ligação própria; sem âncoras aninhadas.",
"countsAndDescriptions": "Preservados dados recebidos, incluindo zero e descrições ausentes; não importados textos ou registos do outro projecto.",
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"supportCard": "Superfície clara para contraste com o título vermelho."
},
"assets": [
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\agronegocio.jpg",
"sourceBytes": 444057,
"original": "outputs/home-sectors-20261023/originals/agronegocio.jpg",
"sha256": "7337b689a6ad8b0b4c8d716ea356db14e22bd2982089825bb2382121ed3482f6",
"derivatives": [
{
"file": "public/images/sectors/agronegocio.webp",
"width": 960,
"height": 540,
"bytes": 110310,
"sha256": "3e9d3be22556300f880be39837f86e764e3d05433beff1da6694ef6cee1d5c34"
},
{
"file": "public/images/sectors/agronegocio-480.webp",
"width": 480,
"height": 270,
"bytes": 31592,
"sha256": "fd1f7624f7fd9224fc1c7bd11746806e419c645ba43c257b1edd9ba250cd47f1"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\economia-azul.jpg",
"sourceBytes": 456892,
"original": "outputs/home-sectors-20261023/originals/economia-azul.jpg",
"sha256": "1a2c04d0d9ed4a26129553daefd2ee18f6d820026ab1203acf3bcf91b0550f69",
"derivatives": [
{
"file": "public/images/sectors/economia-azul.webp",
"width": 960,
"height": 540,
"bytes": 107626,
"sha256": "bf3751e66839c1b3c585c91ffe65d32b9ee79ebd7d08e5e2c95c6cd581b04f8f"
},
{
"file": "public/images/sectors/economia-azul-480.webp",
"width": 480,
"height": 270,
"bytes": 29350,
"sha256": "aaf8ee66709d8e15c4304690dd03830b4368d14468a597d5617ccdb71d614331"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\energia.jpg",
"sourceBytes": 386678,
"original": "outputs/home-sectors-20261023/originals/energia.jpg",
"sha256": "a164e2c8ef155a6a198ae400feca132e79ba75e022473893ec48ebfa0557f679",
"derivatives": [
{
"file": "public/images/sectors/energia.webp",
"width": 960,
"height": 540,
"bytes": 81236,
"sha256": "b5605d9bb8645c5042baa511aaf39fcb187080a48d8e067b130fc974c4c9196d"
},
{
"file": "public/images/sectors/energia-480.webp",
"width": 480,
"height": 270,
"bytes": 21926,
"sha256": "7e53977fc6ce11755455f8092ac3f8c7126f54d909a3935e522f8dae0ab834be"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\industria.jpg",
"sourceBytes": 357131,
"original": "outputs/home-sectors-20261023/originals/industria.jpg",
"sha256": "d70c4257ac58a4b5e9f8b8b1a4cf6d98c0514043442c3ce46f5c98399038527f",
"derivatives": [
{
"file": "public/images/sectors/industria.webp",
"width": 960,
"height": 540,
"bytes": 80422,
"sha256": "ca8ddd2d65647f1ed7a9906271fa2ea3d027f310b89434fd7781a37a3d18322d"
},
{
"file": "public/images/sectors/industria-480.webp",
"width": 480,
"height": 270,
"bytes": 25194,
"sha256": "8bd695231209c0042b94e68b0e2095fa7202297708dabff0f78b337500b8c9b7"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\infraestruturas.jpg",
"sourceBytes": 395383,
"original": "outputs/home-sectors-20261023/originals/infraestruturas.jpg",
"sha256": "ac6cde89507af68991faebee19540f955146724d3be901839b85a4098fe1674b",
"derivatives": [
{
"file": "public/images/sectors/infraestruturas.webp",
"width": 960,
"height": 540,
"bytes": 86654,
"sha256": "14754a59477e5c62a62c190be812c89d9616608b3c98acb6100ad70d94d34df0"
},
{
"file": "public/images/sectors/infraestruturas-480.webp",
"width": 480,
"height": 270,
"bytes": 25772,
"sha256": "eca58096087d05dae3eea2b4d063c103f9fc85b0ea20fc5051313cf9bfa1020f"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\logistica-transportes.jpg",
"sourceBytes": 413702,
"original": "outputs/home-sectors-20261023/originals/logistica-transportes.jpg",
"sha256": "5976ead23da0d4e7d5debf875262f0229d389826aaeb7e7be4ebbac3f5129a9e",
"derivatives": [
{
"file": "public/images/sectors/logistica-transportes.webp",
"width": 960,
"height": 540,
"bytes": 96254,
"sha256": "0d235e7bbd160cccd3f97daa3337d6b27fde49db9782ad2ba0599c6a83022b6c"
},
{
"file": "public/images/sectors/logistica-transportes-480.webp",
"width": 480,
"height": 270,
"bytes": 28816,
"sha256": "32f5137e0a7f6a7894b186edb2ca38f12cdfdca04dda3588de3969fb3d2e333a"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\mineracao.jpg",
"sourceBytes": 415161,
"original": "outputs/home-sectors-20261023/originals/mineracao.jpg",
"sha256": "7e6d1e5a17d434456ad937680a6c5501bd33bd624d2cc7ebf4b643185a9440aa",
"derivatives": [
{
"file": "public/images/sectors/mineracao.webp",
"width": 960,
"height": 540,
"bytes": 96642,
"sha256": "12c93df378f54108b13260c10d29789564af7844cc84bcd9281d2be917d83354"
},
{
"file": "public/images/sectors/mineracao-480.webp",
"width": 480,
"height": 270,
"bytes": 26034,
"sha256": "be3e7763fb8cd2e02f576204269821c40d93a7c155d8088a5084064b7269e4d6"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\outros-sectores.jpg",
"sourceBytes": 328152,
"original": "outputs/home-sectors-20261023/originals/outros-sectores.jpg",
"sha256": "4aaac0869dc7e61c4c7a87e1af95beb108aa9be0da082702f6f70722e62b1e7a",
"derivatives": [
{
"file": "public/images/sectors/outros-sectores.webp",
"width": 960,
"height": 540,
"bytes": 75414,
"sha256": "f47b3d0da5b0cd5153d393c6f779484ba89043e50195e968338108e893a88077"
},
{
"file": "public/images/sectors/outros-sectores-480.webp",
"width": 480,
"height": 270,
"bytes": 24936,
"sha256": "a4fc00551d75c96a1648ad4f81d759d4d6ecd7e6fc5689689216a96934bd1ea2"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\servicos-financeiros.jpg",
"sourceBytes": 348386,
"original": "outputs/home-sectors-20261023/originals/servicos-financeiros.jpg",
"sha256": "f932a56248d5544bff92aa30f4af3a5a63044c311969d4370d06f157b097d570",
"derivatives": [
{
"file": "public/images/sectors/servicos-financeiros.webp",
"width": 960,
"height": 540,
"bytes": 72144,
"sha256": "15379485db7de7b2efebe8bdbd4cbd8e911eb34ab8a23b3dd114c849bb4ae65c"
},
{
"file": "public/images/sectors/servicos-financeiros-480.webp",
"width": 480,
"height": 270,
"bytes": 20698,
"sha256": "c21949201e35e63625d3d2e7520e10912fe65be1186954142b2784f6efca51ed"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\tic-economia-digital.jpg",
"sourceBytes": 417398,
"original": "outputs/home-sectors-20261023/originals/tic-economia-digital.jpg",
"sha256": "b09703bd079f55fb5a8ec3167ce96e1fdb1e5090813a81c817b9a33909427d94",
"derivatives": [
{
"file": "public/images/sectors/tic-economia-digital.webp",
"width": 960,
"height": 540,
"bytes": 97892,
"sha256": "b1d79c1a941085582d6a72799140bafed1bf38ec0201a4dfe5f6faf54ea55ae8"
},
{
"file": "public/images/sectors/tic-economia-digital-480.webp",
"width": 480,
"height": 270,
"bytes": 28242,
"sha256": "c4279a27ed402ca2feb62f11a8a954cbcae993912d3c8c27a2d9644fdf922114"
}
]
},
{
"source": "D:\\Projects\\Tempo_Investidor\\tempo-dashboard\\storage\\app\\public\\investor-portal\\sectors\\turismo.jpg",
"sourceBytes": 393339,
"original": "outputs/home-sectors-20261023/originals/turismo.jpg",
"sha256": "1a76db252bc8c1a9dc1f3c650bd95e55337c36a6f604753d61cad96c19b1875f",
"derivatives": [
{
"file": "public/images/sectors/turismo.webp",
"width": 960,
"height": 540,
"bytes": 93234,
"sha256": "de46707dfdd8b98c48ec88d19f3df0b1d6c130adfcbd97d5a7c2e6fa2db62bef"
},
{
"file": "public/images/sectors/turismo-480.webp",
"width": 480,
"height": 270,
"bytes": 27012,
"sha256": "4d3d9a04b9426474dca9ba8895021b48fffbe7e4d4a4551272b8c832cf83488e"
}
]
}
],
"assetMapping": {
"agronegocio": "agronegocio",
"turismo": "turismo",
"infraestruturas": "infraestruturas",
"economia-azul": "economia-azul",
"mercado-financeiro": "servicos-financeiros",
"mineracao": "mineracao",
"energia-especifica": "energia",
"imobiliario": "outros-sectores",
"bens-de-consumo-e-franquias": "industria",
"tecnologias-de-informacao": "tic-economia-digital"
},
"assetSource": "JPG locais do projecto de referência indicado pelo proprietário, inventariados e copiados sem alteração; WebP derivados para apresentação. Sem alegação adicional de licença externa, origem oficial ou localização documental. Imagem genérica reutilizada como alternativa funcional e para sectores sem imagem específica.",
"assetBytes960": 997828,
"assetBytes480": 289572,
"verification": {
"browser": {
"results": [
{
"width": 320,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 2,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"width": 390,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 2,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"width": 768,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 3,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"width": 1024,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 5,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"width": 1440,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 5,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"width": 1920,
"homeSectors": 5,
"catalogueSectors": 10,
"homeColumns": 5,
"headings": [
{
"title": "Encontre o contexto do seu investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Sectores de investimento",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Como Investir",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Oportunidades em destaque",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Documentos essenciais",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
},
{
"title": "Apoio ao Investidor",
"size": "24px",
"weight": "700",
"color": "rgb(237, 28, 36)",
"lineHeight": "32px"
}
],
"status": "passed"
},
{
"fail": "primary",
"status": "passed"
},
{
"fail": "all",
"status": "passed"
},
{
"emptyCatalogue": true,
"status": "passed"
}
],
"errors": [],
"navigation": [
"/sectores",
"/sectores",
"/sectores",
"/sectores",
"/sectores",
"/sectores/agronegocio",
"/sectores",
"/sectores/turismo",
"/sectores",
"/sectores/infraestruturas",
"/sectores",
"/sectores/economia-azul",
"/sectores",
"/sectores/mercado-financeiro",
"/sectores",
"/sectores/mineracao",
"/sectores",
"/sectores/energia-especifica",
"/sectores",
"/sectores/imobiliario",
"/sectores",
"/sectores/bens-de-consumo-e-franquias",
"/sectores",
"/sectores/tecnologias-de-informacao",
"/sectores",
"/sectores/mercado-financeiro/fintech",
"/sectores",
"/",
"/sectores",
"/sectores"
],
"databaseAccess": false
},
"script": "tools/portal-home-sectors-qa.mjs",
"screenshots": [
"outputs/home-sectors-20261023/catalogue-1024.png",
"outputs/home-sectors-20261023/catalogue-1440.png",
"outputs/home-sectors-20261023/catalogue-1920.png",
"outputs/home-sectors-20261023/catalogue-320.png",
"outputs/home-sectors-20261023/catalogue-390.png",
"outputs/home-sectors-20261023/catalogue-768.png",
"outputs/home-sectors-20261023/fallback-all.png",
"outputs/home-sectors-20261023/fallback-primary.png",
"outputs/home-sectors-20261023/home-sectors-1024.png",
"outputs/home-sectors-20261023/home-sectors-1440.png",
"outputs/home-sectors-20261023/home-sectors-1920.png",
"outputs/home-sectors-20261023/home-sectors-320.png",
"outputs/home-sectors-20261023/home-sectors-390.png",
"outputs/home-sectors-20261023/home-sectors-768.png",
"outputs/home-sectors-20261023/homepage-1440.png"
],
"existingFrontendTests": {
"command": "node --test tests/frontend/portal-api.test.mjs tests/frontend/portal-navigation.test.mjs",
"passed": 20
},
"types": {
"command": "npm run types:check",
"exitCode": 0
},
"lint": {
"exitCode": 0
},
"build": {
"command": "npm run build",
"exitCode": 0,
"assets": {
"resources/js/pages/PortalInvestidor.tsx": "assets/PortalInvestidor-Q1OJxvfZ.js",
"resources/js/pages/portal/services.tsx": "assets/services-DiuG4Zkv.js"
},
"log": "outputs/home-sectors-20261023/build-local.log"
}
},
"scope": {
"appliedLocally": true,
"databaseChanged": false,
"migrationsRun": false,
"businessRulesChanged": false,
"routesChanged": false,
"sourceProjectChanged": false,
"newDependencies": false,
"externalDeployment": false
},
"limits": "Interface e navegação exercitadas com API e páginas públicas em fixtures; não equivale a demonstração integral dos workflows operacionais ou de critérios mais amplos da matriz.",
"matrixReferences": [
"MASTER-efd550fd-0104",
"MASTER-9f5719c8-0077"
]
}
@@ -0,0 +1,406 @@
{
"date": "2026-10-23",
"decision": "Pedido expresso da Tempo: analisar e acrescentar imagens adequadas exclusivamente aos três cartões de Como Investir.",
"analysisAndPlan": "docs/investmoz/public-portal-sections.md#fotografias-nas-etapas-de-como-investir--23102026",
"source": {
"type": "Fotografias ilustrativas geradas por IA, sem atribuição a pessoas, instituições, locais ou projectos reais.",
"tool": "image_gen, modo integrado",
"skill": "C:/Users/jorge/.codex/skills/.system/imagegen/SKILL.md",
"externalPhotographsUsed": false
},
"images": [
{
"key": "explorar",
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 1 EXPLORE opportunities, sector and territory: two Mozambican business professionals, a woman and man in smart casual clothing, standing outdoors by a quiet access road at a modest business park, naturally discussing a tablet held by the woman. Generic unbranded low warehouse buildings and green productive land visible in background, credible southern African environment. Eye-level medium-wide candid composition, both people three-quarter view, looking at tablet and surroundings rather than camera. Business evaluation and discovery, no construction work or hard hats.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-42eb9338-4350-40bf-9098-858c12994189.png",
"original": "outputs/investment-steps-20261023/originals/explorar.png",
"originalSha256": "a3c4ec1a0d2deaf87e0c8e765616c74ab70067eade9a862effce9e6befbbccb6",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-steps/explorar.webp",
"width": 960,
"height": 540,
"bytes": 52266,
"sha256": "c3e151ae649ce3cb66f5babc39978fa5803d320cacc95a27e798db4ec2d45a4c"
},
{
"file": "public/images/investment-steps/explorar-480.webp",
"width": 480,
"height": 270,
"bytes": 19312,
"sha256": "d3f383d9a58b23fada37472ea853f985aca8c3146123747d777b27704d32c7e0"
},
{
"file": "public/images/investment-steps/explorar-fallback.webp",
"width": 240,
"height": 135,
"bytes": 5300,
"sha256": "ba12ac4d2e81ac8a2c2e681667ccb08a3abfdba24de39e6e942d6378b700f2a9"
}
]
},
{
"key": "preparar",
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 2 PREPARE investment and organize documents: a Mozambican woman entrepreneur and a male business adviser seated at a light wood office desk, calmly reviewing and sorting a small tidy set of document folders and loose sheets beside a closed laptop. One points to a page while the other organizes a folder. No visible readable writing or personal data; papers at an angle. Bright modest professional meeting room, view from slightly above table height, hands and folders clear in foreground, natural candid interaction, realistic anatomy.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-5f07618a-1e86-42d9-aaf8-3acd0abf2164.png",
"original": "outputs/investment-steps-20261023/originals/preparar.png",
"originalSha256": "679a6875bb3ec79a9b71c27c9d785334376746be2bb305f461db9e19b0a0b7ea",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-steps/preparar.webp",
"width": 960,
"height": 540,
"bytes": 57942,
"sha256": "6de28042d3abce238a3680fb878779682f3672c3feb97b7e42b72e9a0a0ffbac"
},
{
"file": "public/images/investment-steps/preparar-480.webp",
"width": 480,
"height": 270,
"bytes": 19950,
"sha256": "671ba0fc207f85833b91027d3f6dad91599e95702cfea439e558906f92a2bfac"
},
{
"file": "public/images/investment-steps/preparar-fallback.webp",
"width": 240,
"height": 135,
"bytes": 6008,
"sha256": "645b34cd60b31fdc8e6586dedb71061c08add9ff985ce90909333b0bd1e0aa3e"
}
]
},
{
"key": "acompanhar",
"prompt": "Use case: photorealistic-natural. Asset for a small navigation card on a Mozambique investor portal. Create one individual photorealistic editorial photograph, horizontal 16:9, professional restrained style, natural soft daylight, realistic skin and materials, neutral colors, documentary composition readable at 350px wide. Illustration of a plausible setting, no identifiable named project or official building. No text, lettering, logos, watermarks, flags, maps, diagrams, graphic overlays, montage, or advertisement styling. Keep main subject inside the central 80% for responsive crop. Stage 3 FOLLOW UP submitted request and protocol: a Mozambican male entrepreneur seated at a tidy desk using a laptop to check an online application, one hand naturally on trackpad, a small closed document folder nearby. Three-quarter side view showing focused face and laptop, warm daylight from side window, modest contemporary office with soft greenery out of focus. Laptop screen is oblique and softly out of focus with only indistinct neutral interface rows, no readable text, official seal, logo or invented dashboard details. Convey calm digital self-service and progress checking, not a call centre or signing documents.",
"source": "C:\\Users\\jorge\\.codex\\generated_images\\01a167bd-3f26-71a0-8ec3-3f17519d2bfd\\exec-f896c5f8-19a0-49b0-af8e-0d12bfe4c7aa.png",
"original": "outputs/investment-steps-20261023/originals/acompanhar.png",
"originalSha256": "17d453445f2afb4c7641909950d2ecfb8276c433a4e28eeee94d0a57d1bdad46",
"originalDimensions": {
"width": 1672,
"height": 941
},
"derivatives": [
{
"file": "public/images/investment-steps/acompanhar.webp",
"width": 960,
"height": 540,
"bytes": 43722,
"sha256": "fe764c8161de36ee597f856face5341260670d1a28ab482bd01a0f5120ad0588"
},
{
"file": "public/images/investment-steps/acompanhar-480.webp",
"width": 480,
"height": 270,
"bytes": 16176,
"sha256": "79ceaecb0ef23c6729e43d04b7e91493d6784ea9f658d540dda3550702dc3623"
},
{
"file": "public/images/investment-steps/acompanhar-fallback.webp",
"width": 240,
"height": 135,
"bytes": 5298,
"sha256": "cd4530cfef4f19f23f640ceae59cfd73d0ee269643118b458f2a4748f5845fc6"
}
]
}
],
"cards": [
{
"title": "1. Explorar",
"description": "Identifique o sector, território e oportunidade.",
"href": "/oportunidades",
"image": "explorar"
},
{
"title": "2. Preparar",
"description": "Conheça o percurso e organize os documentos.",
"href": "/como-investir",
"image": "preparar"
},
{
"title": "3. Acompanhar",
"description": "Envie o pedido e acompanhe o protocolo.",
"href": "/balcao-virtual",
"image": "acompanhar"
}
],
"presentation": {
"component": "resources/js/components/investment-steps.tsx",
"homeIntegration": "resources/js/pages/PortalInvestidor.tsx",
"aspectRatio": "16:9",
"responsiveImages": [
960,
480
],
"fallback": "Variante própria de 240 px; se também falhar, espaço reservado com descrição acessível e Imagem indisponível.",
"columns": "Uma coluna abaixo de 768 px, três a partir de 768 px.",
"compactText": "16 px de preenchimento, sem altura mínima de texto.",
"accessibility": "Alt em português, nomes e descrições das âncoras, foco visível, dimensões reservadas e loading lazy.",
"headings": "Título vermelho partilhado da home preservado."
},
"verification": {
"browser": {
"results": [
{
"width": 320,
"dpr": 1,
"fail": "none",
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.7820512820512822
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.7820512820512822
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.7820512820512822
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 390,
"dpr": 2,
"fail": "none",
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.7755102040816326
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.7755102040816326
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.7755102040816326
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 768,
"dpr": 1,
"fail": "none",
"columns": 3,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.7734375
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.7734375
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.7734375
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 1024,
"dpr": 1,
"fail": "none",
"columns": 3,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.7784090909090908
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.7784090909090908
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.7784090909090908
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 1440,
"dpr": 2,
"fail": "none",
"columns": 3,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.775
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.775
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.775
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 1920,
"dpr": 1,
"fail": "none",
"columns": 3,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-480.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.775
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-480.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.775
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-480.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.775
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 390,
"dpr": 2,
"fail": "primary",
"columns": 1,
"photos": [
{
"src": "http://127.0.0.1:5174/images/investment-steps/explorar-fallback.webp",
"alt": "Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.",
"lazy": "lazy",
"ratio": 1.7755102040816326
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/preparar-fallback.webp",
"alt": "Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.",
"lazy": "lazy",
"ratio": 1.7755102040816326
},
{
"src": "http://127.0.0.1:5174/images/investment-steps/acompanhar-fallback.webp",
"alt": "Empreendedor consulta um pedido no computador portátil numa secretária de escritório.",
"lazy": "lazy",
"ratio": 1.7755102040816326
}
],
"layoutShift": false,
"overflow": false
},
{
"width": 390,
"dpr": 2,
"fail": "all",
"columns": 1,
"photos": [],
"layoutShift": false,
"overflow": false
}
],
"errors": [],
"navigation": [
"/oportunidades",
"/como-investir",
"/balcao-virtual"
],
"databaseAccess": false
},
"script": "outputs/investment-steps-20261023/check.mjs",
"screenshots": [
"outputs/investment-steps-20261023/cards-1024-none.png",
"outputs/investment-steps-20261023/cards-1440-none.png",
"outputs/investment-steps-20261023/cards-1920-none.png",
"outputs/investment-steps-20261023/cards-320-none.png",
"outputs/investment-steps-20261023/cards-390-all.png",
"outputs/investment-steps-20261023/cards-390-none.png",
"outputs/investment-steps-20261023/cards-390-primary.png",
"outputs/investment-steps-20261023/cards-768-none.png"
],
"lint": {
"exitCode": 0
},
"types": {
"command": "npm run types:check",
"exitCode": 0,
"log": "outputs/investment-steps-20261023/types.log"
},
"build": {
"command": "npm run build",
"exitCode": 0,
"homeAsset": "assets/PortalInvestidor-C1QTZ3-t.js",
"log": "outputs/investment-steps-20261023/build-local.log"
}
},
"scope": {
"appliedLocally": true,
"otherSectionsChanged": false,
"supportAndFooterChanged": false,
"databaseAccess": false,
"businessRulesChanged": false,
"routesChanged": false,
"newDependencies": false,
"externalDeployment": false
},
"matrixReferences": [
"MASTER-efd550fd-0104"
],
"limits": "Componentes reais com API sintética e destinos interceptados para verificar a navegação; não demonstra os workflows operacionais completos nem encerra o critério mais amplo da matriz."
}
@@ -0,0 +1,32 @@
{
"target": "https://teste-investidor.tempo.co.mz",
"authorization": "Tempo: rever e disponibilizar a entrega consolidada pelo processo Git existente; 23/10/2026",
"base": "e1f010f941e9993645e131b34ec1f4c3fe82c71b",
"continuity": "3d877ec7",
"qa_recovery_record": "115c7731",
"served_commit": "7c1f532134e4492dec010d0c511f669a1e2c23a7",
"finished_at": "2026-10-23T13:16:11.213670+00:00",
"status": "installed",
"release": "/var/www/investmoz-homologacao/releases/7c1f532134e4492dec010d0c511f669a1e2c23a7-524bd081",
"previous_release": "/var/www/investmoz-homologacao/releases/e1f010f941e9993645e131b34ec1f4c3fe82c71b-e905ae1f",
"manifest_sha256": "e72d2fc5343a7c3bb0f9c4b2f3f3c7660b3c6e2db5ba4995875c8130d021fd68",
"database_migrations": 0,
"content_imports": 0,
"permission_changes": 0,
"git": "Fast-forward integration; no remote-exclusive main changes; no force push or patch reapplication",
"initial_failure": "115c7731 preserved the active release: one report test detected omission of an associated inactive historical document. Corrected in 7c1f5321, retaining history without requiring the inapplicable document.",
"verification": {
"affected_local_tests": 17,
"types": "passed",
"automatic_frontend_tests": {"total": 204, "passed": 204, "failed": 0},
"served_revision": "Exact commit confirmed through public build/revision.json",
"public_home": "HTTP 200 without environmental HTTP authentication",
"portfolio": {"public_records": 29, "missing_approved_ids": 0, "changed_approved_titles": 0},
"assets": "Detail, context entry/editor and simulator assets HTTP 200",
"source_hashes": "Five affected server/report files match local Git release bytes",
"persistent_runtime": "Existing environment and storage remain symlinks; no replacement of data, uploads or secrets",
"private_workflow": "Accepted synthetic QA proofs reused; no real dossier writes or new destination business-flow test"
},
"rollback": "Existing pipeline rollback to preserved previous release; retain database, encrypted payloads, uploads, permissions and later history; no database restore",
"remaining": "Transversal adaptation of the rest of the portal is partial; absent portfolio associations are not inferred"
}
@@ -0,0 +1,333 @@
{
"status": "installed_in_homologation_territories_recovered_delta_verified",
"date": "2026-10-23",
"base": "8036b9b0",
"accepted_delivery_receipt": "cdf8fcfb",
"active_comparison": {
"database": {
"database": "teste_investidor",
"port": 5432,
"role": "teste_investidor_app"
},
"read_only": "on",
"published_fiches": 29,
"cluster_relations": 13,
"project_locations": 0,
"RLS": [
{
"relname": "projects",
"relrowsecurity": false,
"relforcerowsecurity": false
},
{
"relname": "project_locations",
"relrowsecurity": false,
"relforcerowsecurity": false
},
{
"relname": "cluster_projects",
"relrowsecurity": false,
"relforcerowsecurity": false
},
{
"relname": "investment_dossiers",
"relrowsecurity": false,
"relforcerowsecurity": false
},
{
"relname": "opportunity_projects",
"relrowsecurity": false,
"relforcerowsecurity": false
}
],
"missing_accepted_locations": 28,
"source_inventory": "inventory-20261007-172113.json",
"cause_scope": "Transported project identities omit accepted territorial rows and citation metadata; visibility recovery only restored publication status. Exact event/time not established."
},
"qa_recovery": {
"mode": "applied",
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"qa": true,
"manifest_sha256": "dc3ae2014da51005d95780e2e4ae87e55e000e66e0765f0671c6e44a958026a0",
"preview_sha256": "601a8603cf39f51a22cef9195e238a21c1552cb3303672b78ec52826c6342b57",
"relations": 28,
"projects": 13,
"relations_restored": 28,
"projects_updated": 13,
"new_dossiers": 0,
"publication_changes": 0
},
"tests": {
"php": {
"tests": 4,
"assertions": 116,
"passed": 4
},
"frontend": {
"tests": 2,
"passed": 2
},
"typescript": "passed",
"build": "passed",
"phpstan_new_recovery_service": "passed, zero errors"
},
"backup": {
"stage": "/var/backups/investmoz-anchor-integration-20261023/backup",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 174,
"files_verified": 369,
"dump_sha256": "207ff14c6af7c4b2dd280def898945cb58314e01930b51c50d502e95cbe6ac68",
"files_sha256": "8cd85ab7d5a772452111e4fe7feab48f7fc694ee3f0fbd264f50096254835fe2",
"restore_verified": false
},
"isolated_restore": {
"restore_verified": true,
"tables_verified": 174,
"container_network": "none",
"database": "anchor_restore_20261023",
"port": 55441,
"source_dump_sha256": "207ff14c6af7c4b2dd280def898945cb58314e01930b51c50d502e95cbe6ac68",
"active_database_writes": 0
},
"preserved_closures": [
"6b544548",
"d8224ba8",
"55136b44",
"8036b9b0"
],
"not_repeated": [
"native saved PDF",
"Word",
"ZIP",
"accepted portfolio via opportunity branch",
"accepted complements and four consumers"
],
"deferred": [
"statistics",
"AI"
],
"pending": [],
"screenshots": [
"outputs/anchor-integration-20261023/qa-sector-location.png",
"outputs/anchor-integration-20261023/qa-anchor-fiche.png",
"outputs/anchor-integration-20261023/qa-anchor-dossier.png",
"outputs/anchor-integration-20261023/qa-anchor-simulator.png",
"outputs/anchor-integration-20261023/homologation-sector-location.png",
"outputs/anchor-integration-20261023/homologation-anchor-fiche.png"
],
"visual_QA": {
"anchor_code": "A06",
"anchor_fiche": "57a4ee7a-4c07-4e26-b8f5-e07726e41512",
"case_id": "3fd1bac6-80cf-48c8-97f3-eb4bb3398a17",
"simulation_draft_id": "bbc9ae3d-8c0e-45d2-b93f-9eaa85bab4d4",
"sector": "Agronegócio",
"province": "Sofala",
"offer_invented": false,
"deliberate_title_preserved": true,
"source_link_visible_in_dossier_and_simulator": true,
"manifestation": "HTTP directed proof, including ownership, same-source identity and versioned binding; browser native confirmation cancelled after control timeout; no formal submission"
},
"served_commit": "cf2ddf21a1457f4852fe6aa10069ec3b854f1567",
"functional_commits": [
"3fd5128c",
"cf2ddf21"
],
"installation": {
"service": "ExecMainStatus=0\nActiveState=inactive\nSubState=dead\n",
"installed": {
"status": "installed",
"commit": "cf2ddf21a1457f4852fe6aa10069ec3b854f1567",
"release": "/var/www/investmoz-homologacao/releases/cf2ddf21a1457f4852fe6aa10069ec3b854f1567-7749f82e",
"previous_release": "/var/www/investmoz-homologacao/releases/3fd5128c62296bc5c85dcd19f1458a7949de5784-803cd41e",
"database_migrations": 0,
"content_imports": 0
},
"latest_report": {
"status": "installed",
"commit": "cf2ddf21a1457f4852fe6aa10069ec3b854f1567",
"release": "/var/www/investmoz-homologacao/releases/cf2ddf21a1457f4852fe6aa10069ec3b854f1567-7749f82e",
"previous_release": "/var/www/investmoz-homologacao/releases/3fd5128c62296bc5c85dcd19f1458a7949de5784-803cd41e",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"cf2ddf21a1457f4852fe6aa10069ec3b854f1567"
],
"finished_at": "2026-10-23T17:21:57.897758+00:00"
},
"log_file": "1792776037181186915.log",
"summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.44s",
"OK (57 tests, 309 assertions)",
"# tests 216",
"# pass 216",
"# fail 0"
],
"log_bytes": 92267
},
"recovery_preview": {
"mode": "preview",
"database": "teste_investidor",
"port": 5432,
"qa": false,
"manifest_sha256": "d2893d0a638d630435aca1890f79a8094dc58eb65bbdd65a470bb685fa075190",
"preview_sha256": "f59e324222ea480d176f1e8e6ae164efa184904c8244873d3fcf6af8497e0dac",
"relations": 28,
"projects": 13
},
"recovery_applied": {
"mode": "applied",
"database": "teste_investidor",
"port": 5432,
"qa": false,
"manifest_sha256": "d2893d0a638d630435aca1890f79a8094dc58eb65bbdd65a470bb685fa075190",
"preview_sha256": "f59e324222ea480d176f1e8e6ae164efa184904c8244873d3fcf6af8497e0dac",
"relations": 28,
"projects": 13,
"relations_restored": 28,
"projects_updated": 13,
"new_dossiers": 0,
"publication_changes": 0
},
"installed_delta": {
"published_fiches": 29,
"territorial_links": 28,
"affected_projects": 13,
"cluster_relations": 13,
"dossier_rows_unchanged": true,
"clusters_corridors_opportunity_relations_unchanged": true,
"historical_territorial_link_identities_preserved": true,
"only_project_metadata_version_timestamp_changed": true,
"functional_file_hashes_match_Git": {
"app/Http/Controllers/InvestmentPortfolioController.php": true,
"app/Http/Controllers/InvestorContextController.php": true,
"app/Services/ApprovedAnchorTerritories.php": true,
"app/Services/InvestorContext.php": true,
"resources/js/lib/investor-context.ts": true,
"resources/js/lib/project-dossier-report.ts": true,
"resources/js/lib/search-territory.ts": true,
"resources/js/pages/investment/submission.tsx": true,
"resources/js/pages/investor-path/context-start.tsx": true,
"resources/js/pages/investor-path/context.tsx": true,
"resources/js/pages/investor-path/show.tsx": true,
"resources/js/pages/portfolio/index.tsx": true,
"resources/js/pages/portfolio/show.tsx": true,
"routes/web.php": true,
"tools/restore-approved-anchor-territories.php": true
},
"pipeline_summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.44s",
"OK (57 tests, 309 assertions)",
"# tests 216",
"# pass 216",
"# fail 0"
],
"table_preservation": {
"tables": 174,
"changed_tables": {
"audit_events": {
"before": {
"rows": 632,
"checksum": "d4f2e8b30c014080b33a588eb0d8afe2"
},
"after": {
"rows": 645,
"checksum": "88c2047620eb3fad4d2a2837ed3daef5"
}
},
"cache": {
"before": {
"rows": 104,
"checksum": "463aff806806b9fa2e4dff3310f516ec"
},
"after": {
"rows": 104,
"checksum": "4963d5a25fecf3352cf0d7aee73a1e63"
}
},
"project_locations": {
"before": {
"rows": 0,
"checksum": "d41d8cd98f00b204e9800998ecf8427e"
},
"after": {
"rows": 28,
"checksum": "80b4e44b11208a023fcfb989e738b95a"
}
},
"projects": {
"before": {
"rows": 16,
"checksum": "af127d727cfc1050c26b99130e397109"
},
"after": {
"rows": 16,
"checksum": "17fb48a69b09e1e8ec81b0d30e29ce80"
}
},
"sessions": {
"before": {
"rows": 11,
"checksum": "912e728cc332bfc41a7e0f94c84e118d"
},
"after": {
"rows": 15,
"checksum": "713242c22c549258fab61d1b9cd993ef"
}
}
},
"unchanged_tables": 169
},
"exact_29_approved_fiche_identities_preserved": true,
"synthetic_fiches_transported": 0
},
"Git_preservation": {
"accepted_base_receipt": "cdf8fcfb",
"previous_installed": "8036b9b0",
"candidate": "cf2ddf21a1457f4852fe6aa10069ec3b854f1567",
"protected_files_unchanged": {
"app/Services/InvestmentComplements.php": true,
"app/Services/InvestmentConsumerScope.php": true,
"app/Services/InvestorPath.php": true,
"app/Services/SimulationFormDrafts.php": true,
"resources/js/components/investment-complements.tsx": true,
"resources/js/components/dossier-prefill.tsx": true,
"resources/js/components/investment-requirements.tsx": true
},
"matrix_changed_rows_since_accepted_receipt": [
"AUD-REL-02",
"AUD-LEG-06"
],
"other_matrix_records_preserved": 3421,
"accepted_receipt_updates_preserved": [
"AUD-SIM-INT03",
"AUD-SIM-INT06"
],
"schema_migrations_added": [],
"stats_AI_code_changed": false,
"force_push": false
},
"manifest_verification": {
"Git_LF_sha256": "d2893d0a638d630435aca1890f79a8094dc58eb65bbdd65a470bb685fa075190",
"initial_QA_Windows_bytes_sha256": "dc3ae2014da51005d95780e2e4ae87e55e000e66e0765f0671c6e44a958026a0",
"canonicalization": "CRLF to LF only; identical JSON and accepted relations",
"first_active_preview": "refused before writes; corrected canonical preview and apply succeeded"
},
"next": "Tempo final navigation and presentation evaluation",
"not_a_claim_of_complete_transversal_implementation": true,
"installed_public_delta": {
"catalogue": "https://teste-investidor.tempo.co.mz/carteira?kind=project&sector=Agro-ind%C3%BAstria%20e%20agricultura&province_id=f57ff041-6275-57b9-8800-c35e46e0d475",
"results": 1,
"fiche": "57a4ee7a-4c07-4e26-b8f5-e07726e41512",
"source": "portfolio:57a4ee7a-4c07-4e26-b8f5-e07726e41512",
"territories_visible": 4,
"search_return_preserves_filters": true,
"preparation_GET_same_identity_and_province": true,
"homologation_test_dossiers_or_submissions_created": 0
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
{
"target": "https://teste-investidor.tempo.co.mz",
"authorization": "Tempo, 23/10/2026: concluir visual QA, integrar pelo Git e conferir apenas os tres deltas instalados",
"status": "installed",
"commit": "6b544548d3c83fc8a72ab65d8f58f84685a2877f",
"finished_at": "2026-10-23T13:45:19.950347+00:00",
"manifest_sha256": "65a694b5d81f833446a308f48f4966b32a8bdc5421f1ee2196b79ac0b798b636",
"release": "/var/www/investmoz-homologacao/releases/6b544548d3c83fc8a72ab65d8f58f84685a2877f-b38d94da",
"previous_release": "/var/www/investmoz-homologacao/releases/7c1f532134e4492dec010d0c511f669a1e2c23a7-524bd081",
"git": "Fast-forward from 7c1f5321; no remote-exclusive main changes; no force push",
"database_migrations": 0,
"content_imports": 0,
"permission_changes": 0,
"automatic_frontend_tests": {"tests": 206, "passed": 206, "failed": 0},
"qa_visual_completed": "Beira page displays seven documented opportunities; Florestas opens and both preparation links retain from_dossier",
"destination_delta": {
"language": "Existing private TESTE DE HOMOLOGACAO dossier: Portuguese (Mozambique) selected, explicit save confirmed, page reload recovered the same language without false conflict",
"test_dossier": "958df138-7980-4685-8389-fa90be02ed6b",
"simulation_draft": "6c07a968-7ff9-42fa-b252-072bb1acf221",
"report": "Printed report revision 4 distinguishes title from user dossier data, summary inherited from Florestas opportunity version 2, sector from sector catalogue and Sofala from territorial catalogue",
"beira": "Seven detail pages opened from observed links; exact titles and from_dossier preserved in preparation links",
"beira_titles": ["Açúcar — Corredor da Beira", "Carne bovina — Corredor da Beira", "Florestas — Corredor da Beira", "Hortícolas — Corredor da Beira", "Milho, trigo e arroz — Corredor da Beira", "Sésamo — Corredor da Beira", "Soja — Corredor da Beira"],
"stale_versions": "Accepted QA rejection tests reused; installed server guard bytes match the tested version. No deliberate stale write against destination user data",
"version_guard_sha256": "1626280b34481298de964ff82027ae7ea9b1aa80d1b86554987edbe6ec5a087f",
"draft_client_sha256": "dfe1a44e8a9af48735d75d0742583ce2280abf7615049d569f7963763bdd5e4c"
},
"local_proofs": ["outputs/preservation-inventory-20261023/beira-seven-visual-QA.png", "outputs/preservation-inventory-20261023/installed-language-recovery-6b544548.png", "outputs/preservation-inventory-20261023/installed-provenance-report-6b544548.png", "outputs/preservation-inventory-20261023/installed-beira-seven-6b544548.png"],
"preservation": "No republication of the 34 opportunities or reimport of portfolio; only existing labelled private test dossier used for writes; previous release preserved",
"rollback": "Existing release rollback; retain database, uploads, permissions, history and later edits",
"remaining": "Transversal application remains partial; this receipt closes only the three installed correction deltas"
}
@@ -0,0 +1,253 @@
{
"date": "2026-10-23",
"timezone": "Africa/Johannesburg",
"status": "installed_in_homologation_delta_verified",
"baseline_commit": "4098192a056959d788e6f9ff5cd501211b001f39",
"preserved_acceptance": {
"delivery": "6b544548d3c83fc8a72ab65d8f58f84685a2877f",
"receipt": "4098192a056959d788e6f9ff5cd501211b001f39",
"cases_repeated": false
},
"cause": {
"portfolio_field": "content.focus",
"previous_contract": "string rendered as descriptive text; no complementary selection or typed origin-target relationship in the dossier",
"data_loss_claimed": false
},
"scope": {
"components": [
"irrigation",
"storage",
"cold",
"processing",
"spinning",
"logistics",
"certification",
"training",
"finance"
],
"existing_opportunity_identity_preserved": true,
"documented_sources": [
"portfolio associations including the existing accepted documentary membership",
"public complementary_project_links",
"existing publicly disclosed value-chain references",
"published offering partnerships explicitly associated with the origin"
],
"distinct_states": [
"registered opportunity",
"identified complementary need without a matched offer",
"existing service or instrument",
"previously selected relationship requiring review"
],
"dossier": [
"encrypted existing payload",
"draft",
"preview and confirmation",
"explicit removal and reselection",
"existing audit history",
"source and territory preserved",
"pending edits saved before complementary navigation"
],
"consumers": [
"dossier",
"shared prefill component for simulator, land, desk and submission",
"simulator context snapshot",
"shared report and export rows"
]
},
"validation": {
"database": "simulator_fiscal_delta_20261021_test",
"host": "127.0.0.1",
"port": 55441,
"database_identity_guard": true,
"test_transaction_rollback": true,
"feature_test_file": "tests/Feature/InvestmentComplementsTest.php",
"feature_tests_passed": 8,
"feature_assertions": 134,
"frontend_test_file": "tests/frontend/investment-complements.test.mjs",
"frontend_tests_passed": 2,
"typescript": "passed",
"build": "passed",
"php_syntax": "passed",
"diff_whitespace_check": "passed",
"public_chain_branch": "synthetic public projection contract; not evidence of new real-world chain associations",
"browser_visual_verification": "passed_for_the_requested_complement_scope"
},
"visual_verification": {
"date": "2026-10-23",
"environment": "http://127.0.0.1:18574",
"router": "tools/corridor-agribusiness-qa-router.php",
"database_guard_checked_on_requests": true,
"actor": "existing synthetic QA actor 509; existing normal browser session",
"case_id": "6fdbb963-726e-4048-9884-8cddc04e77a6",
"case_title": "SYNTHETIC QA — conferência visual de complementos 23-10-2026",
"existing_opportunity": {
"id": 286,
"reference": "INV-MZ-GEN-8128691124566423",
"slug": "inv-mz-gen-8128691124566423",
"result": "Opened the existing fiche from the storage complement; the preparation entry retained the portfolio origin and the existing identity. No reimport or duplicate."
},
"need_without_identified_offer": {
"code": "spinning",
"origin": "portfolio:a7271336-808b-42b4-8bfd-5b49c72dbe2b",
"result": "Started idea preparation in the same dossier; recovered as a complementary need with location still undetermined, without inventing an offer or inheriting Sofala."
},
"associated_service": {
"id": 17,
"synthetic_fixture": true,
"origin": "opportunity:309",
"territory": "Gaza",
"result": "Opened the existing QA service fiche, selected it through the return action, confirmed it and recovered it in the same dossier."
},
"multiple_opportunities": {
"first": {
"id": 286,
"territory": "Sofala"
},
"second": {
"id": 309,
"territory": "Gaza",
"synthetic_fixture": true
},
"complementary_opportunity": {
"id": 310,
"origin_id": 309,
"territory": "Gaza",
"synthetic_fixture": true
},
"service_origin_id": 309,
"recovered_after_confirmation_and_browser_reload": true,
"complements_assigned_to_first_automatically": false
},
"fixture_preparation": {
"previous_service_offerings_in_qa": 0,
"new_synthetic_records": "Two projects, two opportunities, one explicit complementary-project link and one associated training offering, solely in the guarded QA database; no grants or real publication/approval claims.",
"preexisting_records_preserved_by_hash": {
"opportunities": 45,
"projects": 17,
"investment_dossiers": 30,
"portal_partnerships": 0
},
"fixtures_and_visual_case_persisted_in_qa": true,
"fixture_receipt": "outputs/complements-visual-20261023/fixture.json"
},
"evidence_directory": "outputs/complements-visual-20261023",
"automated_suites_repeated": false,
"functional_code_changes_in_this_visual_pass": false,
"does_not_demonstrate": [
"multi-opportunity zones and parks",
"specific per-opportunity context in land assistance",
"specific per-opportunity context in virtual desk",
"specific per-opportunity context in manifestation",
"availability of a real training or financing service in homologation"
],
"evidence_files": [
{
"path": "outputs/complements-visual-20261023/existing-opportunity.png",
"bytes": 112261,
"sha256": "93425703b17c0976a5728acc7448faa0b7dd73e34973cb68ebf72cb9d416dd58"
},
{
"path": "outputs/complements-visual-20261023/associated-service.png",
"bytes": 117223,
"sha256": "a5d16746b4264b779430cc3843959492bea41a3da2adc00298fa2133c99aef9d"
},
{
"path": "outputs/complements-visual-20261023/recovered-opportunities.png",
"bytes": 99910,
"sha256": "eb65dd0d42af0c9f553db6756760dcc16f598b2da7b3d0157b6faea3dbebf24e"
},
{
"path": "outputs/complements-visual-20261023/recovered-complements.png",
"bytes": 147972,
"sha256": "4e2db825710665298803542fe9c7f299404962503609948ddd083c3daf195a73"
},
{
"path": "outputs/complements-visual-20261023/recovered-visible-state.json",
"bytes": 2304,
"sha256": "d2ada1b501db973f1492a54f1704e9cb2de112e38cacd4ab916f059dbbd11f18"
},
{
"path": "outputs/complements-visual-20261023/fixture.json",
"bytes": 904,
"sha256": "79fa2ac8196ca5620b258c78332a2462b44f2c5b622fc0c1895fb39199efcd67"
}
]
},
"preservation": {
"active_database_writes": false,
"homologation_installation": false,
"migration": false,
"publication": false,
"import": false,
"new_roles_or_grants": false,
"new_matrix_or_backlog": false
},
"matrix_rows": [
"RF-CLU-003",
"RF-COR-004",
"AUD-REL-02",
"AUD-LEG-06",
"AUD-SIM-INT06"
],
"limitations": [
"Visual verification is limited to the documented QA scenarios; no Tempo acceptance or destination installation is claimed.",
"The resolver covers the nine requested component scopes in available documentary contexts; unmatched expressions and absent relationships are not fabricated.",
"No matched offer means none identified in the examined relationships, not proof that the entire collection has no offer.",
"The existing first-opportunity zone/park selection and complete per-opportunity requirements in other forms remain partial.",
"No global completion of the adaptive journey is claimed."
],
"installation": {
"status": "installed_delta_verified",
"date": "2026-10-23",
"target": "https://teste-investidor.tempo.co.mz",
"served_commit": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"installer": {
"status": "installed",
"commit": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"release": "/var/www/investmoz-homologacao/releases/d8224ba84b56482e0291bc18bb1c9c29316b19b9-c0d7c675",
"previous_release": "/var/www/investmoz-homologacao/releases/6b544548d3c83fc8a72ab65d8f58f84685a2877f-b38d94da",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"d8224ba84b56482e0291bc18bb1c9c29316b19b9"
],
"finished_at": "2026-10-23T15:12:40.124788+00:00"
},
"public_revision": {
"commit": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"branch": "main",
"built_at": "2026-10-23T15:12:37.169518+00:00",
"manifest_sha256": "52a021c1281e72d2aabe4a8fad82a150857e851d917c28e6898d2d439dfb771f"
},
"verification": {
"qa_guard_assertions": 3106,
"unit_tests": 57,
"unit_assertions": 309,
"frontend_tests": 208,
"frontend_failures": 0,
"public_real_fiche_opened": "INV-MZ-GEN-6048328347519124",
"real_target_id": 48,
"origin_preserved": "portfolio:a7271336-808b-42b4-8bfd-5b49c72dbe2b",
"spinning": "need_without_identified_offer_location_undefined",
"service": "existing_publication_states_no_synthetic_fixture_transferred",
"manual_business_cases_repeated": false
},
"preservation": {
"concurrent_main": "43cd0be7cbe2450f1928b059f1456454f23943ef",
"integration": "088f2c2cf47f5b16f351e231c490e848a664e433",
"accepted_delivery": "6b544548",
"accepted_receipt": "4098192a",
"database_migrations": 0,
"content_imports": 0,
"qa_fixture_payload_shipped": false,
"transversal_coverage": "partial"
},
"screenshot": {
"path": "outputs/complements-install-20261023/installed-complements.png",
"sha256": "9d48d2a51d5fbb1c0f5e92247af399c582444b329406416c172848ee7b1cb1e4"
},
"pending": [
"multi-opportunity specific contexts in zones/parks, Terra/DUAT, Balcao and manifestation remain to be demonstrated"
]
}
}
@@ -0,0 +1,89 @@
{
"date": "2026-10-23",
"status": "installed_in_homologation_delta_verified",
"functional_commit": "1afdfee8",
"base": "bc84fbdb",
"accepted_deliveries_preserved": [
"6b544548",
"d8224ba8",
"55136b44"
],
"qa": {
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"guard": "asserted in tests and fixture preparation",
"case_id": "d279172b-8bd1-4074-b000-ee25f0d29778",
"actor": "synthetic existing QA actor 509",
"browser": "Edge http://127.0.0.1:18574",
"existing_accepted_cases_modified": false
},
"changes": [
"simulator explicit or inherited opportunity key saved and recovered",
"applicability facts reasons and stage details by opportunity without replacing common data",
"land shortcut filters dossier owner and chosen opportunity",
"requirement and return links preserve scope",
"authorised common contact proposals where duplication was found",
"Word/ZIP/print composition identifies selected scope and applicable stages"
],
"verification": {
"http_tests": 7,
"http_assertions": 170,
"http_suite": "tests/Feature/InvestmentTreatmentScopeTest.php",
"frontend_tests": 2,
"frontend_suite": "tests/frontend/treatment-scope-export.test.mjs",
"typescript": "passed",
"production_build": "passed",
"visual": "new QA dossier second Manica / first Sofala / second; general simulator selection and scoped return to dossier"
},
"remaining": [
{
"scope": "native PDF",
"status": "only saved native PDF pagination and legibility remain unverified; HTML is not that proof",
"limitation": "browser-only control does not expose system print/save dialogue; no failure attributed to application"
},
{
"scope": "anchor projects sector and location search with manifestation and investment preparation continuity",
"status": "to analyse; no inference of implementation or defect",
"matrix": "AUD-REL-02"
}
],
"limits": [
"no homologation deployment or active data/schema/config/permission change",
"no repeat of accepted complement or four-consumer tests",
"no new visual proof of every alternative entry or removal editor; directed HTTP proof only",
"no statistics or AI work",
"temporary fixture clone location confirmation and test version field corrected before passing results"
],
"artifacts": [
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-dossier.png",
"sha256": "2b5f65064c80e376efdb2d12c87725c042027296f1e4ab6536f140462482562f"
},
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-dossier.txt",
"sha256": "0887a3ab5da2c6eccd369c7ca691df9d5f8d92279f6202eddf15b1652c138c89"
},
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-simulator.txt",
"sha256": "410f91d3afe220ec46f435c9de62d34671b2096d961527d0ce97a3edb9838b34"
},
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-exports/second.docx",
"sha256": "7aeadecb3684d000d5fe0c3b93d339f380c434daf3890ae24bd79c2f723ae40b"
},
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-exports/second.zip",
"sha256": "a3c1100d9c9ffc75fbda0f0489479c17275e8fa0b3025ad3ae2ecb293a9aa1f8"
},
{
"path": "outputs/multi-opportunity-fix-20261023/treatment-exports/second-print.html",
"sha256": "dffff89a9d4f186698bbc077a8a5031bd95290e12bcdba7b00ec7cd1a095bc4a"
}
],
"installation": {
"status": "installed_in_homologation_delta_verified",
"commit": "8036b9b071229dbf7e85ea748db70abcbeefc379",
"receipt": "treatment-scope-installation-receipt-20261023.json",
"delta_verified": true
}
}
@@ -0,0 +1,123 @@
{
"scope": "Shared administrative maps stabilization",
"source_revision": "cf2ddf21",
"candidate": "066707ec",
"owner_decision": "Tempo 2026-10-24: public consultation of the exact accepted administrative contours; originals private, no parcels or other internal layers",
"diagnosis": {
"data_loss": false,
"common_consumers": "Public-only filter excluded all 408 internal administrative boundaries",
"administration": "Legacy manage-portal gate excluded both approved superusers",
"comparison": "No change in common geometry service/controller/components between 30aed7cd and cf2ddf21",
"base_map": "Provider tiles and served assets respond; browser framing is checked after installation"
},
"qa": {
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"tests": 6,
"assertions": 64,
"result": "passed",
"phpstan": "passed",
"writes": "Only isolated QA transactions, rolled back"
},
"baseline": {
"published_fiches": 29,
"territorial_relations": 28,
"geometries": 408,
"geometry_rows_checksum": "7d8b71d3e65d89ffdc43c748b0572dbc"
},
"backup": {
"tables": 174,
"files_verified": 369,
"restore_tables_verified": 174,
"restore_network": "none",
"dump_sha256": "2fb5b4488799bb6072c38ee41ccf91fc2a55561363c19e280ab066363d9e0647"
},
"public_manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc",
"installed": {
"status": "installed",
"commit": "066707ece631f0a7c963031c211844fbdd68916b",
"release": "/var/www/investmoz-homologacao/releases/066707ece631f0a7c963031c211844fbdd68916b-6b7bb5bd",
"previous_release": "/var/www/investmoz-homologacao/releases/cf2ddf21a1457f4852fe6aa10069ec3b854f1567-7749f82e",
"database_migrations": 0,
"content_imports": 0
},
"deferred": [
"Statistics",
"AI"
],
"accepted_tests_repeated": false,
"public_projection": {
"available": 408,
"levels": {
"admin_post": 264,
"district": 133,
"province": 11
},
"failures": [],
"manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"preservation": {
"tables": 174,
"changed_tables": {
"cache": {
"before": {
"rows": 104,
"checksum": "5d172b4d252c7b025f23ac70cf570cb6"
},
"after": {
"rows": 104,
"checksum": "a60f3e47e1ae28937404f05f45e33135"
}
},
"sessions": {
"before": {
"rows": 6,
"checksum": "dcdb7eba3f3e8f29f40ec3f7542be6ce"
},
"after": {
"rows": 7,
"checksum": "0059c0b0e16d532c6658fd420582f2e9"
}
}
},
"unchanged_tables": 172
},
"installed_file_sha256": {
"app/Services/ApprovedAdministrativeBoundaries.php": "401fd62795ca8f33106be6e07130d9207d6661695440bfc40f62f6f6b42bec64",
"app/Services/TerritoryGeometry.php": "00ce3e6dde92868b0d34b963b8e59a26e6663149cdc0398c645ef636ca382966",
"app/Http/Controllers/InvestmentTerritoryController.php": "3b184c6ea1b97f0abe86d01567d3d73444c49192c638478a6d53619e63d5d18d",
"app/Http/Middleware/EnsureScopedPortalAdministration.php": "1474db760f6bd6003a5325ae721698e90e2709a2354b51abde77f920a9dad81a",
"docs/investmoz/unification/approved-administrative-boundaries-20261024.json": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"pipeline_summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.58s",
"OK (57 tests, 309 assertions)",
"# tests 216",
"# pass 216",
"# fail 0"
],
"proofs": {
"anonymous_http": {
"status": 200,
"territory_id": "dd219b89-cc17-5944-8c7d-d2766c82b526",
"selected": "DONDO",
"ancestor": "Sofala",
"areas": 2
},
"browser": {
"province_to_district": "passed",
"framing": "selected district with ancestor context",
"reload_preserves_selection": true,
"zoom": "passed",
"visible_tiles_loaded": 36,
"visible_tiles_failed": 0,
"screenshot": "outputs/map-stabilization-20261024/dondo-installed.png",
"screenshot_sha256": "7569f3b66aceb7ef9816c31bfaa172ebcdfa10e4911961b67a129a654b5cd349"
},
"business_rows_unchanged": true,
"active_synthetic_requests_created": 0
},
"status": "installed_delta_verified",
"remaining_in_scope": []
}
@@ -0,0 +1,95 @@
{
"date": "2026-10-23",
"status": "installed_in_homologation_delta_verified",
"target": "https://teste-investidor.tempo.co.mz",
"installed_commit": "55136b446deb710adf58ce2460e571bb19e94375",
"functional_commit": "4a01e9e2b7362282f844a8c6e0b26380ab0b71e0",
"qa_receipt_commit": "55136b446deb710adf58ce2460e571bb19e94375",
"diagnosis_commit": "ad4160f04074eb54ee3e5da213ba1cba884f02de",
"previous_served_commit": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"integration": {
"mode": "normal fast-forward push to existing main; no force",
"concurrent_main_at_integration": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"previous_history_preserved": true,
"schema_configuration_dependency_delta": false,
"fixtures_or_data_payload_shipped": false
},
"public_revision": {
"commit": "55136b446deb710adf58ce2460e571bb19e94375",
"branch": "main",
"built_at": "2026-10-23T15:55:10.921100+00:00",
"manifest_sha256": "b2dd6172516f220344ede5fc9d7876b7fc8417416d03842d737452709379a12c"
},
"installed_delta": {
"browser": "Edge; existing normal homologation session",
"existing_opportunity_id": 53,
"existing_reference": "INV-MZ-GEN-8326396601945337",
"existing_case": "958df138-7980-4685-8389-fa90be02ed6b",
"links": [
{
"href": "/investidor/terra-duat/novo?context=958df138-7980-4685-8389-fa90be02ed6b&opportunity=opportunity%3A53",
"text": "Terra/DUAT desta oportunidade"
},
{
"href": "/balcao-virtual/dossiers/958df138-7980-4685-8389-fa90be02ed6b?opportunity=opportunity%3A53",
"text": "Balcão desta oportunidade"
}
],
"authenticated_scoped_get": {
"case_id": "958df138-7980-4685-8389-fa90be02ed6b",
"version": 5,
"opportunities": [
{
"key": "opportunity:53",
"territory": "Sofala",
"park": null
}
],
"land": [
{
"province_id": "f57ff041-6275-57b9-8800-c35e46e0d475"
}
],
"desk_territory": "f57ff041-6275-57b9-8800-c35e46e0d475",
"submission_location": "Sofala",
"scoped_snapshot_choices": [
"opportunity:53"
]
},
"new_requests_created": 0,
"requests_saved_or_submitted": 0,
"accepted_business_cases_repeated": false,
"qa_proof_reused": "multi-opportunity-context-qa-receipt-20261023.json",
"installer": "Existing Git-triggered isolated build and atomic promotion; public served revision confirms completion. No manual edit of served directory."
},
"preservation": {
"protected_files_unchanged": {
"app/Services/InvestmentComplements.php": true,
"resources/js/components/investment-complements.tsx": true,
"docs/investmoz/unification/continuity-corrections-installation-receipt-20261023.json": true
},
"complements_delivery": "d8224ba8",
"previous_closes": [
"6b544548",
"4098192a"
],
"qa_fixtures_remain_qa_only": true,
"real_publication_states_not_changed": true
},
"deferred": [
"statistics",
"AI"
],
"limits": [
"Installed delta only; does not claim a new business acceptance or repeat second-opportunity QA cases.",
"No new native PDF acceptance.",
"Overall transversal implementation remains partial beyond this delivered scope."
],
"rollback": "Existing atomic pipeline rollback, retaining shared database, uploads, configuration and later history; prior served reference d8224ba8.",
"evidence_sha256": {
"installed-context-summary.json": "7f167ff8e13455eacf3a326aa0d0644870f849af33d0744e941f6afa9feefe9e",
"installed-links.json": "6de8a43461394870bb5b8f0462b13a71c29610c843d029d9bae32177849748f4",
"installed-scope.png": "04b1c7290305f8e2bafbfdd2fb77bb1d38207b3d30763dd3576e5aea154358aa",
"public-revision.json": "ffc2c30c02ced31797ed900f25dbb05c0274e2ff4a9cdb8bc0ef6ed7deddc653"
}
}
@@ -0,0 +1,118 @@
{
"date": "2026-10-23",
"status": "executable_correction_verified_in_isolated_qa_not_installed",
"functional_commit": "4a01e9e2b7362282f844a8c6e0b26380ab0b71e0",
"branch": "codex/opportunity-consumer-scope-20261023",
"diagnosis_commit": "ad4160f0",
"preserved_delivery": "d8224ba8",
"preserved_closes": [
"6b544548",
"4098192a"
],
"cause_and_correction": {
"parks": "Per-choice park reference and validation against that choice corridor and territory. Own lookup refreshes on own corridor/site. Previous common park is retained on its original first choice when reading legacy data.",
"consumer_scope": "Explicit opportunity key is resolved within the authorised dossier. Known fiche entry retains that key; multiple choices without a key show a chooser and propose only common data. No positional fallback for the three consumers.",
"persistence": "Canonical server-generated snapshot in each existing encrypted payload: identity, territory, corridor, park, source/version and scoped requirements. Client snapshots are replaced; foreign dossier identities are rejected.",
"fields": "Existing inheritance reconciliation retains deliberate local edits and parcel details. Returned desk request field restrictions retain locked association/snapshot.",
"reporting": "Land printable report displays saved identity and context; desk receipt displays saved scope; manifestation revision and intake metadata retain canonical association; common dossier report rows pair each park with its own opportunity."
},
"qa": {
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"browser": "Edge",
"url": "http://127.0.0.1:18574",
"actor_id": 509,
"new_case": "1017980b-a61c-44a4-b8d5-bd909c2d1316",
"first_choice_exactly_preserved": true,
"second_change": "309: Gaza/Limpopo/QA Gaza park -> Manica/Beira/QA Manica park",
"consumers": {
"land": {
"key": "opportunity:309",
"territory": "Manica",
"park": "SYNTHETIC QA — parque de ensaio Manica",
"context_revision": 2,
"requirement_keys": [
"opportunity:309"
]
},
"desk": {
"key": "opportunity:309",
"territory": "Manica",
"park": "SYNTHETIC QA — parque de ensaio Manica",
"context_revision": 2,
"requirement_keys": [
"opportunity:309"
]
},
"submission": {
"key": "opportunity:309",
"territory": "Manica",
"park": "SYNTHETIC QA — parque de ensaio Manica",
"context_revision": 2,
"requirement_keys": [
"opportunity:309"
]
}
},
"temporary_fixtures": "Three clearly marked synthetic parks only in isolated QA. Existing opportunity identities 286/309 reused; no new opportunity or service. Accepted complements case left intact."
},
"checks": {
"feature_tests": 4,
"feature_assertions": 104,
"frontend_tests": 3,
"typescript": "passed",
"build": "passed",
"browser": [
"second-only edit, park options, confirmation and recovery",
"land entry from identified second choice, save, recovery and printable report",
"desk scoped entry, draft save and reload",
"manifestation from existing fiche, explicit dossier binding, draft save and reload",
"unscoped multi-choice entry asks for a choice; selecting second recovers its Manica scope"
],
"additional_checks": [
"canonical snapshot rejects client forgery",
"foreign identity and other actor rejected",
"intermediate unknown territory draft recovered without restoring the source location",
"returned request preserves locked association",
"desk receipt template renders canonical second scope; manifestation revision retains association; report park rows retain both identities"
],
"limits": [
"No external submission or institutional decision was made. Desk receipt template was exercised with an isolated transaction fixture, not claimed as a real receipt.",
"Land HTML printable report was visually checked; no new native PDF export acceptance is claimed.",
"Accepted complement functional evidence was reused; no new visual round of those cases.",
"Initial test invoker used default arguments and started the default suite; it was interrupted and fixed to target this file. That incomplete run is not acceptance evidence."
]
},
"preservation": {
"protected_files_unchanged": {
"app/Services/InvestmentComplements.php": true,
"resources/js/components/investment-complements.tsx": true,
"docs/investmoz/unification/continuity-corrections-installation-receipt-20261023.json": true
},
"accepted_case_choices_and_sites": true,
"accepted_complements_retained": 4,
"migrations": 0,
"content_imports": 0,
"active_database_writes": 0,
"publication_changes_in_homologation": 0,
"qa_fixtures_shipped": false
},
"homologation": {
"unchanged": true,
"served_commit": "d8224ba84b56482e0291bc18bb1c9c29316b19b9",
"main_not_updated": true
},
"evidence_directory": "outputs/multi-opportunity-fix-20261023",
"evidence_sha256": {
"desk-recovered.txt": "a294e3d1a439e7ff42b66c59e9b4edc4a960a606384880faba02b78a4da884cc",
"explicit-choice-selected.txt": "f2a7432816b3b39881111ea7f4ccb2ac29a586ac407e5cd1a0f54f9b901cd359",
"explicit-choice.txt": "437ea9df20c437b8781c2279c8aa0f847a4dc45ffb1d3d0b455e4520b2c38297",
"land-report.png": "a586050af1f58d279fe2733f03861bf8f8d4e3f76e5076aee2e013a9ca7f4802",
"land-report.txt": "b274f72934d4717ffc08c75dc6574840b7f0b0ec0e6e987ebc52ce1fa56549e5",
"submission-recovered.png": "5dc5da5ff7bdb48a06649fd287e5c22f606e8bcc8035331821c0cd0c4cc0668e",
"submission-recovered.txt": "ac64e391cdaaa59a479e0ff3a829dc6daddf40d0d28b89a940cdfc35f223d6e1",
"verified-state.json": "a709caae0e6877128848dbb51d7bca929fd8457ace4331a565b422edef4e290f",
"visual-fixture.json": "6828f245430b938f0f6253186e81211af26641d959af0d7321d7c0d51bec5d55"
},
"transversal_status": "partial; this receipt covers the four named QA consumers only"
}
@@ -0,0 +1,122 @@
{
"date": "2026-10-23",
"status": "assessment_completed_gaps_confirmed_not_corrected",
"code_revision": "af9261809c6fece9ea3c7c79b49a80562f2ace68",
"accepted_complements_delivery": "d8224ba8",
"prior_closes": [
"6b544548",
"4098192a"
],
"accepted_cases_repeated": false,
"homologation_changed": false,
"probe": {
"date": "2026-10-23",
"qa_database": "simulator_fiscal_delta_20261021_test",
"qa_port": 55441,
"case_id": "6fdbb963-726e-4048-9884-8cddc04e77a6",
"transaction_rolled_back": true,
"first_choice_exactly_preserved": true,
"second_before": {
"key": "opportunity:309",
"territory": "Gaza",
"corridor": "limpopo"
},
"second_after_recovered": {
"key": "opportunity:309",
"territory": "Manica",
"corridor": "beira"
},
"third_choice_preserved": true,
"consumers": {
"land": {
"unchanged_after_second_change": true,
"before": {
"project_reference": "SYNTHETIC QA — conferência visual de complementos 23-10-2026",
"activity": "Agronegócio",
"purpose": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"parcels": [
{
"province_id": "f57ff041-6275-57b9-8800-c35e46e0d475"
}
]
},
"after": {
"project_reference": "SYNTHETIC QA — conferência visual de complementos 23-10-2026",
"activity": "Agronegócio",
"purpose": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"parcels": [
{
"province_id": "f57ff041-6275-57b9-8800-c35e46e0d475"
}
]
}
},
"desk": {
"unchanged_after_second_change": false,
"before": {
"title": "SYNTHETIC QA — conferência visual de complementos 23-10-2026",
"territory_id": "f57ff041-6275-57b9-8800-c35e46e0d475",
"location_source": "Referência consultada no contexto v2: Sofala. Não confirma disponibilidade.",
"description": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"sector": "Agronegócio",
"province_id": "f57ff041-6275-57b9-8800-c35e46e0d475"
},
"after": {
"title": "SYNTHETIC QA — conferência visual de complementos 23-10-2026",
"territory_id": "f57ff041-6275-57b9-8800-c35e46e0d475",
"location_source": "Referência consultada no contexto v3: Sofala. Não confirma disponibilidade.",
"description": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"sector": "Agronegócio",
"province_id": "f57ff041-6275-57b9-8800-c35e46e0d475"
}
},
"submission": {
"unchanged_after_second_change": true,
"before": {
"sector": "Agronegócio",
"objective": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"location": "Sofala"
},
"after": {
"sector": "Agronegócio",
"objective": "Produção, silos, moagem e processamento de milho, trigo e arroz.",
"location": "Sofala"
}
}
},
"parks": {
"ui_request_origin": "choices[0]",
"first_context": {
"corridor": "beira",
"territory": "Sofala"
},
"first_options": [],
"second_options_before": [],
"second_options_after": [],
"separate_park_slot_available": false
},
"revision_before": 2,
"revision_inside_transaction": 3,
"complement_acceptance_retested": false,
"original_case_exactly_preserved": true
},
"cause": {
"parks": "context.tsx derives request parameters and effect dependencies solely from choices[0]; selection.park and server compatibility validation are common and based on the first choice.",
"land": "InvestorContext::prefill generates one parcel from common fields; form.tsx applies only v[0] to the first parcel. No opportunity identity is attached to that proposal.",
"desk": "prefill uses common fields and first territory. A second-choice change updates the context revision text but retains first-choice territory and description.",
"submission": "prefill uses common sector, summary and location. No second-choice scope is supplied.",
"display": "DossierPrefill renders OpportunityChoices only for simulator; the other three consumers show common selection. Complement relations retain their independent existing display and are not retested."
},
"evidence_limits": [
"Commit/recovery exercised through existing application services inside a rolled-back QA transaction, not a new browser-session acceptance.",
"QA park option sets were empty for the sampled locations; no positive park availability or loss of a registered park is claimed.",
"Terra/DUAT current UI inspected read-only; desk and submission conclusions are based on executed prefill and source contracts, not submitted operations.",
"Deliberate form edits, save/recovery of the three destination forms and per-opportunity park selection remain acceptance criteria for a correction."
],
"visual_land": {
"path": "outputs/multi-opportunity-review-20261023/land-visible.txt",
"sha256": "5a2be5f9d0f6f0073d053bfdcb4523cf6e531ad3c929c2dbe6a8d4aaa34dfe4d"
},
"next_correction": "Scope zone selection and destination form proposals to an explicit opportunity identity; preserve other choices and deliberate edits; verify a second-only change and save/recovery in each destination.",
"transversal_status": "partial"
}
@@ -0,0 +1,63 @@
{
"date": "2026-10-23",
"candidate": "1afdfee8",
"native_report_fix": "2856240c",
"deployment_status": "installed_in_homologation_delta_verified",
"origin_main_checked": "55136b44",
"accepted_scopes_retested": false,
"new_test": "tests/frontend/native-pdf-scope.test.mjs: 1 passed",
"types": "passed",
"build": "passed",
"qa_report": {
"revision": 13,
"opportunity": "opportunity:309",
"location": "Manica",
"declared_funding_total": "123456 MZN",
"declared_secured": "45678 MZN",
"expected_gap": "77778.00 MZN",
"native_pdf_expected": "outputs/multi-opportunity-fix-20261023/candidata-1afdfee8.pdf",
"pdf_saved_verified": true,
"native_pdf_actual": "C:\\laragon\\www\\portal-investidor\\outputs\\multi-opportunity-fix-20261023\\Relatório — SYNTHETIC QA - escolha comum aos tres consumidores.pdf",
"native_pdf_sha256": "010eb4ea12015563c1b2f8bc255b1078c276f824c50690732541d5bffe34b8a6",
"native_pdf_pages": 10,
"visual_pagination": "passed pages1-10",
"file_discovery": {
"machine": "ONALA",
"user": "macbook",
"session": "Console",
"extension": ".pdf",
"size_bytes": 191637,
"requested_basename_not_found": "candidata-1afdfee8.pdf",
"actual_basename": "Relatório — SYNTHETIC QA - escolha comum aos tres consumidores.pdf",
"resolution": "existing file discovered by listing the exact requested directory; no rename, copy or PDF regeneration"
}
},
"anchor_analysis": {
"status": "analysed_not_implemented",
"confirmed_code_gaps": [
"sector filter absent from portfolio UI and query contract",
"no direct idea preparation from anchor fiche without an associated opportunity"
],
"directed_verification_remaining": [
"reconcile territorial data with demonstrated and destination state using equivalent access",
"anonymous interest authentication return and same-fiche identity"
],
"existing_accepted_branch": "portfolio origin via associated opportunity to dossier and simulator is preserved, not retested",
"qa_documentary_anchors": 15,
"qa_document_sector_location_region_present": true,
"qa_canonical_primary_territories": 0,
"qa_public_location_links": 0,
"qa_published_opportunity_links": 0,
"synthetic_anchor_excluded": true,
"inventory_sha256": "7d2c0c36fa51b05ce97dfd25c997b77eccf7136a99220ed8607fcd2436fd3d39",
"no_homologation_loss_inferred": true
},
"statistics_and_AI": "deferred",
"installation": {
"status": "installed_in_homologation_delta_verified",
"commit": "8036b9b071229dbf7e85ea748db70abcbeefc379",
"receipt": "treatment-scope-installation-receipt-20261023.json",
"delta_verified": true
},
"status": "installed_in_homologation_delta_verified"
}
@@ -1,5 +1,19 @@
{
"date": "2026-10-23",
"current_public_readonly_confirmation": {
"date": "2026-10-23",
"url": "https://teste-investidor.tempo.co.mz/carteira",
"visible_dossiers": 29,
"projects": 15,
"clusters": 8,
"corridors": 6,
"manifest": "approved-portfolio-visibility-20261023.json",
"missing_manifest_ids": [],
"different_titles": [],
"session_scope": "Authenticated browser reading the public route; prior anonymous recovery proof preserved",
"remote_data_writes": 0,
"remaining_continuity_gap": "from_dossier is validated on opportunity consultation but not carried by the Prepare investment link into the private context"
},
"operation": "Inventory before further acceptance tests; no active data changed",
"baseline_commit": "37a8eb015ac5022fd34bcd89cfe47c9fc61903b0",
"existing_43_components": [
@@ -21,8 +35,9 @@
"number": 3,
"component": "Navegação contextual e manifestação de interesse",
"historic_status": "Disponível",
"current_preservation_status": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino."
"current_preservation_status": "Continuity and multi-opportunity consumers delivered: 55136b44, 8036b9b0, cf2ddf21. Accepted proofs preserved.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino.",
"status_before_stabilization_closure": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora."
},
{
"number": 4,
@@ -161,8 +176,9 @@
"number": 23,
"component": "Cartografia e consumidores territoriais restantes",
"historic_status": "Não demonstrado",
"current_preservation_status": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto."
"current_preservation_status": "Shared maps delivered 066707ec/5d45541a: 408 accepted contours publicly available; originals private, 29 fiches and 28 relations preserved. Missing locality polygons remain future work.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto.",
"status_before_stabilization_closure": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão."
},
{
"number": 24,
@@ -196,8 +212,9 @@
"number": 28,
"component": "Pesquisa — estados do recorte R2",
"historic_status": "Disponível",
"current_preservation_status": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites."
"current_preservation_status": "Accepted search/selectors preserved; portfolio visibility and anchor continuity restored and delivered cf2ddf21. No current portfolio-empty fault.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites.",
"status_before_stabilization_closure": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada."
},
{
"number": 29,
@@ -931,5 +948,79 @@
"new_roles_or_approvers": false,
"new_content_approval": false
},
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key."
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key.",
"stabilization_closure_20261024": {
"basis": "Tempo explicit sequence and technical preservation responsibility, 24/10/2026",
"installed_functional_baseline": "066707ece631f0a7c963031c211844fbdd68916b",
"map_receipt": "5d45541a",
"known_open_faults_in_delimited_stabilization_scope": [],
"does_not_close_all_43_components": true,
"prior_losses_do_not_share_a_proven_single_cause": true,
"accepted_closures": [
"6b544548/4098192a",
"d8224ba8/af926180",
"55136b44/b6a626c9",
"8036b9b0/cdf8fcfb",
"cf2ddf21/c1f4a719",
"066707ec/5d45541a"
],
"documentary_limits": [
"Six legacy opportunity/anchor-cluster pairs without documented identity remain indeterminate: AUD-REL-02",
"Two incomplete legacy originals retain their existing documentary limit"
],
"not_yet_proven_component_numbers": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirement_ids": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"territorial_count_reconciliation": {
"source": "outputs/preservation-inventory-20261023/local.json and homologation.json",
"public_active_local": 1921,
"public_active_target": 1921,
"local_archived_internal_groups": [
32,
32,
5
],
"individual_identity_comparison_of_archived_69_claimed": false,
"reactivation_authorized": false
},
"new_control": {
"test": "tests/Unit/ApprovedAdministrativeBoundariesTest.php",
"automatic_delivery_suite": "Unit",
"tests": 2,
"assertions": 8,
"result": "passed",
"scope": "Approved public snapshot identity; changed geometry/metadata, wrong territory and new internal identity rejected",
"http_access_contract": "tests/Feature/TerritoryGeometryConsumerAccessTest.php",
"http_contract_run_when": "Geometry/access/hierarchy changes, on isolated QA only; existing accepted result reused here"
},
"technical_preservation_owner": "Codex and programmers",
"Tempo_review": "Utility, clarity and presentation; no obligation to rediscover technical regressions",
"sequence": [
"Corrections and stabilization",
"Fiscal simulator with defined scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"accepted_acceptance_tests_repeated": false,
"active_database_writes": 0,
"runtime_application_source_changes": 0
}
}
@@ -0,0 +1,80 @@
{
"reference_commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"runtime_files_changed_since_066707ec": [],
"matrix_records": 3423,
"matrix_columns": 49,
"updated_existing_rows": [
"AUD-GEO-01",
"AUD-GEO-02",
"AUD-GEO-03",
"AUD-REL-01",
"AUD-REL-02",
"AUD-DESK-01",
"AUD-DESK-02",
"AUD-SIM-01",
"AUD-LEG-06"
],
"accepted_receipts_unchanged_sha256": {
"map-stabilization-receipt-20261024.json": "2b69216b644f2244897431a10f3e6828e822d1317fab326a457a5934261c1618",
"anchor-integration-receipt-20261023.json": "86b0d01aa2fb03be84c1b1c6254ea0932abe813375f00560aaf39ce78d199f3d",
"treatment-scope-installation-receipt-20261023.json": "a0904250e1ed201276fce0318b9cc131edf4c8ec0ab6d449536b1e1344906798",
"multi-opportunity-context-installation-receipt-20261023.json": "3e1030de9aafd916252926a33d18cf808a05e389ddce5d9a3cd5230f039bc8f2"
},
"new_tests": {
"tests": 2,
"assertions": 8,
"passed": true
},
"active_database_writes": 0,
"status": "installed_reference_verified",
"date_Africa_Johannesburg": "2026-10-24",
"git_tag": "reference/stabilization-20261024",
"served_revision": {
"commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"branch": "main",
"built_at": "2026-10-24T09:10:08.359035+00:00",
"manifest_sha256": "949d1047867f5879e724e5ddc38d4865b06aeb92641337d3952ea7b5ad55bb7a"
},
"previous_functional_revision": "066707ece631f0a7c963031c211844fbdd68916b",
"verification_method": "New unit control tested; semantic matrix/inventory reconciliation; canonical accepted receipt hashes; empty Git diff for runtime/schema/config/permissions/deploy; public installed revision. Prior accepted technical and visual proofs reused.",
"accepted_tests_repeated_by_this_review": false,
"automatic_installer": "Normal existing pipeline; no extra manual acceptance round",
"fresh_authenticated_database_read_claimed": false,
"business_data_preservation": "Accepted map receipt 5d45541a reused: 29 fiches, 28 links, 408 geometries, 172 business/nonvolatile tables equal; no runtime or data mutation introduced by this reference delta",
"known_open_stabilization_faults": [],
"documentary_pending": [
"AUD-REL-02: six legacy opportunity/anchor-cluster pairs remain indeterminate without documented relationships",
"Two incomplete originals in the legacy collection retain the accepted documentary limit"
],
"not_yet_proven_existing_components": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirements": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"technical_preservation_owner": "Codex and programmers",
"Tempo_evaluation": "Usefulness, clarity, presentation; does not need to rediscover technical regressions",
"next": "Fiscal simulator scope definition; not started by this consolidation",
"reference_sequence": [
"Stabilization",
"Defined fiscal simulator scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"not_complete_portal_acceptance": true
}
@@ -1,5 +1,407 @@
# Rastreabilidade, utilização e formação
## Fecho do lote de estabilização e sequência de referência — 24/10/2026
**Ordem expressa da Tempo:** (1) correcções e estabilização; (2) simulador fiscal com âmbito definido; (3) Imobiliário; (4) Bens de consumo e franquias; (5) Estatística e painéis; (6) IA. A preservação técnica é responsabilidade do Codex e dos programadores. A Tempo avalia utilidade, clareza e apresentação; não tem de redescobrir regressões para comprovar preservação.
Consolidação sobre a revisão instalada `066707ec`, recibo `5d45541a`, mantendo a mesma matriz e o inventário dos 43 componentes. Nesta passagem não foi encontrada nova falha executável nos recortes de estabilização já delimitados. A revisão de código confirma que a primeira oportunidade usada na migração do contexto comum não substitui o âmbito dos consumidores: `InvestmentConsumerScope` filtra a escolha antes do snapshot e dos relatórios. Não foram repetidas as provas aceites, impressões, submissões ou alterações de dados. A confirmação da Tempo anterior baseou-se no relatório comunicado, sem conferência do recibo local; não é registada como uma nova prova técnica ou visual.
| Situação no lote | Percursos e saldo concreto |
| --- | --- |
| Resolvido e entregue | Gravação/idioma, proveniência do relatório e Beira/sete oportunidades: `6b544548`/`4098192a`. Complementos: `d8224ba8`/`af926180`. Zonas/parques, Terra/DUAT, Balcão e manifestação por oportunidade: `55136b44`/`b6a626c9`. Selector do simulador, etapas/requisitos e pedido Terra correcto, incluindo PDF guardado: `8036b9b0`/`cdf8fcfb`. Âncoras, sector/localização e continuidade: `cf2ddf21`/`c1f4a719`. Mapas: `066707ec`/`5d45541a`. Pendências históricas destes trechos ficam superadas pelas respectivas entregas. |
| Falha conhecida ainda aberta neste lote | **Nenhuma identificada nos recortes enumerados.** Não equivale a aceitação integral dos 43 componentes ou de todas as variantes do portal. Não se inventa uma nova correcção para justificar outra ronda. |
| Pendência documental preservada | `AUD-REL-02`: os pares das seis oportunidades legadas com âncoras/clusters continuam indeterminados quando não há relação documentada. Não confundir com as 28 relações recuperadas dos projectos. Preservar a preparação independente; não associar por proximidade ou nome. Dois originais incompletos do acervo legado conservam o limite documental já registado, sem representar perda causada pelos mapas. |
| Ainda sem prova no âmbito próprio | Inventário existente: consolidação de duplicados distintos (componente 6); aplicação/editor e versões de checklists (18); partilha e variantes de sala (21/35); modalidades/SLA e fecho operacional/documental (22/25); acessibilidade além das provas aceites (24). Não foram identificadas novas regressões nesses itens; não os declarar concluídos nem exigir processos reais/sintéticos na homologação para fechar este lote. Mantêm os critérios próprios da matriz, sem atribuição de novo prazo ou prioridade. |
| Desenvolvimento futuro, separado de regressões | Negócios similares por raio (`AUD-MARKET-01`), importador de empresas/empreendimentos (`AUD-ESTABLISHMENT-01`), catálogo de fundos (`AUD-FUND-01`), comparador societário e modelos específicos (`AUD-COMPANY-01/02`), guiões de capitalização/Bolsa (`AUD-CAPITAL-01/02`), polígonos próprios ainda inexistentes e enquadramento Fintech específico. Variantes de Imobiliário/franquias, Estatística/painéis e IA seguem a ordem acima; SuiteCRM mantém a opção anterior de desactivação. Não se declara que todo o código destes módulos esteja ausente. |
**Causas independentes:** os mapas tinham filtro público e dependência de uma permissão antiga; a carteira sofreu perda de visibilidade de registos presentes; as relações de âncoras tinham omissão no transporte entre identidades documentais. As provas delimitam esses casos e não estabelecem a causa de todas as perdas anteriores. O inventário histórico territorial tinha 1 990 linhas locais contra 1 921 no destino: **1 921 públicas activas em ambos, mais 69 internas arquivadas apenas no local**, repartidas em 32+32+5. Isto explica a diferença agregada; não é prova de correspondência individual das 69 nem autoriza reactivá-las. Nenhuma reposição adicional decorre dessa contagem.
### Controlo técnico nas próximas entregas
Reutilizar o inventário/recibos e comparar apenas o delta e as dependências afectadas. O pipeline existente conserva detecção de alterações concorrentes na árvore instalada, contrato de schema, QA isolado, assets imutáveis, troca atómica e reversão por falha de saúde. A suite normal de entrega passa também a incluir `ApprovedAdministrativeBoundariesTest`: identidade aceite e recusa de novo contorno interno, território trocado, geometria ou metadados alterados. É controlo novo de âmbito público, não repetição da demonstração visual aceite. As duas provas novas passaram, com oito asserções e sem acesso à base.
Quando se alterar acesso, hierarquia ou serviço de mapas, executar o delta correspondente de `TerritoryGeometryConsumerAccessTest` em QA isolado; cobre o filtro público e os perfis aprovados sem `manage-portal`, incluindo MFA administrativo. O teste unitário isolado não substitui esse contrato HTTP. Para alterações de contexto, usar os casos afectados de `InvestmentTreatmentScopeTest`, `InvestmentAnchorIntegrationTest` e consumidores/complementos, sem repetir indiscriminadamente toda a suite de aceitação. No destino, comparar identidades/fontes/estado de publicação das 29 fichas, 28 relações e 408 geometrias com os manifestos aceites, configuração e permissões equivalentes; mudanças legítimas posteriores são reconciliadas explicitamente, não sobrescritas. Backup/restauro e acção autenticada precedem qualquer escrita de dados activos.
**Saldo de fecho:** zero falhas técnicas conhecidas abertas neste lote; limites documentais, itens sem prova e desenvolvimento futuro permanecem identificados acima e nas mesmas linhas da matriz. Não se declara a aplicação transversal integralmente concluída. O próximo lote é a delimitação do simulador fiscal; esta passagem não activa cálculos, fontes, estatísticas ou IA. A avaliação de navegação/apresentação da Tempo pode continuar pelo percurso normal e alimenta apenas falhas concretamente observadas.
**Referência consolidada instalada: `276d833a4b4f9071db328b653f48529b2dfac4f9`**, confirmada pelo `/build/revision.json` público. Etiqueta Git `reference/stabilization-20261024`. A comparação Git de `app`, `resources`, `routes`, `database`, `config`, `public` e `deploy` com `066707ec` é vazia; não há novo delta funcional a reensaiar. Os recibos aceites conservam os mesmos bytes canónicos. Mantidas 3 423 linhas lógicas e 49 colunas da matriz; só nove linhas existentes foram reconciliadas. A prova anterior de preservação das 29 fichas, 28 relações e 408 geometrias é reutilizada, não apresentada como nova consulta autenticada da base nesta passagem. [Recibo da referência](stabilization-reference-receipt-20261024.json).
## Mapas — estabilização comum e divulgação delimitada, 24/10/2026
Diagnóstico por leitura autenticada na homologação `cf2ddf21`: **29 fichas, 28 relações territoriais e 408 geometrias válidas**, sem eliminação. Os 408 limites INE/2019 conservam classificação interna, propósito administrativo e SRID 4326. O fingerprint integral das linhas é `7d8b71d3e65d89ffdc43c748b0572dbc`. Não há RLS nas tabelas comparadas. O serviço comum não mudou desde `30aed7cd`: os consumidores gerais pediam exclusivamente geometrias públicas; o cadastro usava ainda `manage-portal`, ausente nos dois superutilizadores aprovados. A prova anterior demonstrava o cadastro privado, não a divulgação pública. O mapa base, tiles e assets carregam; a resposta territorial vazia explica a ausência de contornos e de enquadramento do território escolhido.
**Decisão expressa da Tempo nesta passagem:** «Sim, disponibilizar os contornos administrativos aceites». Implementada uma projecção pública de leitura dos [408 snapshots exactos](approved-administrative-boundaries-20261024.json), conferindo identidade e hashes da geometria original e metadados. Não há reimportação, reclassificação de linhas, publicação de parcelas, acesso aos ficheiros originais ou abertura de outras camadas internas. Alterações posteriores não recebem divulgação automática. Os perfis autorizados reutilizam o acesso privado já aprovado; a leitura administrativa conserva MFA. Respostas privadas não são guardadas em cache partilhada.
QA isolado `simulator_fiscal_delta_20261021_test:55441`: seis testes dirigidos, 64 asserções; selecção, gravação/recuperação no dossier e prefill do simulador com a mesma identidade, leitura pública autorizada, bloqueio de camadas não autorizadas e de snapshots alterados, permissões/MFA e fingerprint original preservado. PHPStan dos quatro ficheiros funcionais passou. Backup fresco: 174 tabelas, 369 ficheiros conferidos; restauro isolado sem rede reproduziu as 174 tabelas.
**Entregue na homologação em `066707ece631f0a7c963031c211844fbdd68916b`**, por avanço normal do Git, preservando `cf2ddf21` e as alterações concorrentes. Instalador: contrato 1/3106, PHP 57/309, frontend 216/216 e build aprovados; zero migrações e zero importações. Os cinco ficheiros funcionais instalados coincidem com o Git. Consulta pública pelo serviço instalado: **408/408 disponíveis**, 11 províncias, 133 distritos e 264 postos, zero falhas. HTTP anónimo recupera Dondo e Sofala pela identidade canónica, com Dondo seleccionado e Sofala como contexto ancestral. Conferência visual: mudança de província para distrito actualiza o contorno e enquadramento; escolha preservada após recarregar, zoom operacional e 36 tiles carregados sem falha na observação final. Houve falhas transitórias de tiles na janela anterior; os mesmos URLs responderam 200 e o recarregamento recuperou a base, sem alteração de configuração ou fornecedor.
Comparação das 174 tabelas: **172 iguais por contagem/hash**, diferenças apenas em `cache` e `sessions`; tabelas de negócio, geometrias, permissões e fontes intactas. Conservados 29 fichas, 28 relações e o fingerprint integral das 408 geometrias. Nenhum dossier/pedido sintético criado na homologação. [Recibo da estabilização](map-stabilization-receipt-20261024.json); prova visual em `outputs/map-stabilization-20261024/dondo-installed.png`. O saldo técnico desta reposição está encerrado; a avaliação de navegação e apresentação pela Tempo permanece o próximo trabalho.
Preservados os fechos `d8224ba8`, `55136b44`, `8036b9b0` e `cf2ddf21`/`c1f4a719`, sem repetição das provas aceites. Localidades sem polígono próprio conservam apenas o contexto ancestral explicitamente identificado; não são inventadas coordenadas ou parcelas. Estatísticas e IA permanecem adiadas. Esta estabilização não declara concluída a aplicação transversal nem substitui a avaliação de navegação e apresentação pela Tempo.
## Projectos âncora — entregues na homologação em cf2ddf21, 23/10/2026
Reconciliado o saldo de `6985f82e` com as provas territoriais de 7 de Outubro e os recibos de reposição. A leitura autenticada da homologação, com o papel da aplicação e transacção apenas de leitura, confirmou **29 fichas publicadas, 13 relações de clusters e zero ligações em `project_locations`**. As cinco tabelas comparadas não usam RLS. Os projectos transportados têm identificadores diferentes dos originais e perderam as 28 ligações territoriais e os respectivos metadados de citação; não desapareceram as 29 fichas. A reposição de visibilidade anterior não abrangia estas relações. A comparação delimita a omissão no transporte; não identifica por si só o instante em que ocorreu.
O [manifesto das 28 relações aceites](approved-anchor-territories-20261023.json) conserva identidades históricas, fontes, citações e papéis primary/secondary/impact, mapeando os projectos e dossiers actuais por identidade documental verificada. A ferramenta `tools/restore-approved-anchor-territories.php` confere hashes, versões, território e prévia; recusa conflitos e exemplares arquivados/restritos; preserva metadados concorrentes, guarda estado anterior e produz auditoria. Não publica fichas nem cria ofertas. Em QA foram repostas 28 relações de 13 projectos; a repetição foi demonstrada sem duplicação. A01/A02 continuam sem implantação individual inventada; Norte da Zambézia conserva o papel de impacto parcial.
Incrementos: pesquisa explícita pelo sector documental em conjunto com a localização; entrada directa da ficha pública para preparar uma ideia, mesmo sem oportunidade publicada; origem `portfolio:<id>` guardada no dossier e no snapshot/relatório do simulador. A cobertura documental não é atribuída como implantação. A localização explicitamente escolhida na pesquisa acompanha a entrada; uma localização já guardada conserva-se, com divergência explicada. Títulos e dados deliberadamente editados mantêm-se na comparação existente. A retoma autenticada da manifestação conserva a mesma ficha e os filtros e não cria pedidos por GET. A preparação iniciada pela manifestação usa a associação já existente ao dossier, com controlo de proprietário e versões; não submete o pedido.
Provas dirigidas: `InvestmentAnchorIntegrationTest` (4 casos, 116 asserções) e `anchor-integration.test.mjs` (2 casos); ver [recibo de QA e reconciliação](anchor-integration-receipt-20261023.json). Mantidos os percursos já aceites de carteira → oportunidade associada → dossier/simulador, complementos `d8224ba8`, consumidores `55136b44` e tratamento `8036b9b0`/recibo `cdf8fcfb`. Nenhuma nova impressão de PDF, prova Word/ZIP ou ronda dos fechos aceites. Estatísticas e IA permanecem adiadas.
**Entregue e delta conferido** na revisão `cf2ddf21a1457f4852fe6aa10069ec3b854f1567`, após integração `3fd5128c` e correcção delimitada da verificação de finais de linha do manifesto entre Windows e Git. A primeira prévia recusou o hash divergente antes de qualquer escrita; a prévia canónica passou. A reposição autenticada recuperou **28 relações em 13 projectos**, com 13 eventos de auditoria, zero novas fichas e zero alterações de publicação. As **29 identidades aprovadas, conteúdos e fontes** permanecem exactamente iguais; preservadas as 13 relações de clusters e os corredores. A comparação das 174 tabelas encontrou 169 iguais por contagem/hash; as diferenças de domínio limitam-se a projectos/metadados, relações territoriais e auditoria; cache e sessões tiveram alterações técnicas. O backup restaurou 174 tabelas iguais numa base isolada sem rede e verificou 369 ficheiros.
Os 15 ficheiros funcionais instalados coincidem com o Git. Instalador: zero migrações e zero importações automáticas; controlos habituais aprovados (contrato 1/3106, PHP 57/309 e frontend 216/216). Conferência pública apenas do delta: pesquisa A06 por sector documental e Sofala encontra a ficha original; quatro localizações e respectivas fontes recuperadas; retorno conserva filtros; a entrada para preparar ideia usa `portfolio:<id>` e a província escolhida. Nenhum dossier, pedido ou manifestação de ensaio foi criado na homologação. Os ensaios sintéticos continuam exclusivamente em QA.
**Saldo técnico deste recorte: sem pendências de código, reposição ou disponibilização identificadas.** Segue a avaliação final de navegação e apresentação pela Tempo. Este fecho não declara implementadas funcionalidades futuras nem a conclusão integral da aplicação transversal; Estatísticas e IA continuam adiadas. O inventário e as provas anteriores permanecem preservados, sem nova ronda dos casos aceites.
## Oportunidade em tratamento — entregue na homologação em 8036b9b0, 23/10/2026
Concluída a disponibilização autorizada da candidata **`1afdfee8`**, incluindo a correcção delimitada do relatório **`2856240c`**, na revisão instalada **`8036b9b071229dbf7e85ea748db70abcbeefc379`**. Integração normal por fast-forward desde `55136b44`, sem force e sem alterações concorrentes em `main` na conferência prévia. [Recibo de instalação e delta](treatment-scope-installation-receipt-20261023.json).
O PDF original guardado foi encontrado pelo nome automático na pasta indicada, conferido em texto e nas **dez páginas renderizadas**, sem o regenerar. Conservados nome, bytes e SHA-256 `010eb4ea12015563c1b2f8bc255b1078c276f824c50690732541d5bffe34b8a6`. A revisão 13 contém a segunda oportunidade/Manica, os cinco requisitos específicos e os valores 123456/45678/77 778,00 MZN, com paginação legível. A diferença do nome literal pedido explica o erro anterior de localização; a máquina, conta, sessão e extensão foram confirmadas.
Instalador: **installed**, conclusão `2026-10-23T16:46:13.798467+00:00`, zero migrations/importações, observador activo e fila vazia. Revisão pública confirmou `8036b9b0`, build `16:46:11.203132 UTC`, manifesto SHA-256 `6021421c4ab12146c5fae9c8966e3dae5c8bd1361132e12184e65e00b7e8951e`. Os **15 ficheiros funcionais** afectados coincidem por hash com os blobs Git instalados. Os controlos automáticos do instalador passaram; não se repetiu a ronda manual de provas aceites.
**Delta instalado conferido por leitura autenticada:** ficha real **Florestas — Corredor da Beira**, oportunidade **53**, dossier existente `958df138-7980-4685-8389-fa90be02ed6b`. Novas ligações Simular/Etapas transportam a mesma chave; selector do dossier mostra 53/Sofala; atalhos preservam o âmbito. GET do contexto devolve `consumer_opportunity=opportunity:53`, snapshot apenas dessa oportunidade e dos seus requisitos. A decisão existente «Não precisa de terra» conserva a dispensa da acção no dossier observado. Versão do dossier **5**, igual ao recibo anterior; não foram criados, gravados ou submetidos pedidos no destino, nem aberta a edição do rascunho privado do simulador. As provas positivas de dois pedidos/duas oportunidades feitas em QA são reutilizadas, sem as reproduzir na homologação.
Complementos `d8224ba8`, fechos `6b544548`/`4098192a` e quatro consumidores `55136b44`/`b6a626c9` preservados; nenhuma fixture sintética, alteração de schema/configuração/permissões, Estatísticas ou IA integra este transporte. Mantidas todas as identidades da matriz e as linhas alheias. O recibo posterior fica na branch de trabalho, sem provocar outra instalação apenas documental.
**Saldo corrente:** esta frente está **entregue na homologação**, com PDF e delta instalados conferidos. A frente dos projectos âncora mantém a análise `6985f82e`: filtro sectorial e ideia directa sem oferta por implementar; reconciliação territorial entre ambientes e prova dirigida de interesse/autenticação por concluir. Não é declarado fecho global do percurso nem realizada antecipadamente a avaliação final pela Tempo. As indicações de «QA», «aguarda PDF» ou «não entregue» nas passagens históricas seguintes estão superadas apenas para este recorte.
## PDF nativo guardado — conferência concluída, 23/10/2026
Conferido o ficheiro **existente**, sem regenerar o PDF ou repetir Word/ZIP/provas aceites: `outputs/multi-opportunity-fix-20261023/Relatório — SYNTHETIC QA - escolha comum aos tres consumidores.pdf`. A pasta era a indicada; o nome foi o título automático do navegador, não `candidata-1afdfee8.pdf`. Leitura na máquina **ONALA**, utilizador **macbook**, sessão **Console**; extensão real `.pdf`, 191637 bytes. O erro anterior era «Cannot find path … candidata-1afdfee8.pdf … because it does not exist» para esse nome literal; a listagem da mesma pasta encontrou o original. Não houve renomeação, cópia nem mudança de máquina.
SHA-256 `010eb4ea12015563c1b2f8bc255b1078c276f824c50690732541d5bffe34b8a6`; PDF 1.4, produtor **Skia/PDF m155**, criado pelo Edge, revisão **13**. Texto e **dez páginas renderizadas do ficheiro guardado** conferidos: oportunidade `309`, Manica/parque correspondente; cinco requisitos próprios e fundamentos, estado **Por iniciar**; pressupostos **123456 MZN**, **45678 MZN**, diferença **77 778,00 MZN**; sem Sofala nem `not_started`. Paginação 1/10–10/10, A4, cabeçalhos de tabela repetidos, linhas completas, margens/rodapés legíveis e sem sobreposição ou corte visível. A última página contém as últimas referências e a indicação dos anexos; não está vazia. As quebras de linha do PDF foram normalizadas apenas para procurar texto, sem alterar o documento.
Prova: `native-pdf-inspection.json`, texto e páginas em `outputs/multi-opportunity-fix-20261023/`; [recibo actualizado](native-pdf-and-anchor-review-20261023.json). A condição de conferência do PDF está satisfeita para a candidata `1afdfee8` com a correcção delimitada `2856240c`. Disponibilização Git autorizada pela Tempo em execução; o fecho de entrega só será registado após confirmar a revisão e o delta instalados. As menções abaixo ao ficheiro em falta são históricas e superadas por esta descoberta/conferência. Análise dos projectos âncora conserva o saldo delimitado em `6985f82e`; Estatísticas e IA continuam adiadas.
## Duas frentes — PDF nativo e análise dos projectos âncora, 23/10/2026
Pedido expresso da Tempo após `32144aa2`: conferir o PDF nativo guardado da candidata `1afdfee8`, depois integrar/disponibilizar pelo Git e conferir só o delta; analisar projectos âncora por sector/localização e continuidade para manifestação, dossier e simulador. Word, ZIP e provas aceites não foram repetidos. Estatísticas e IA permanecem adiadas. Este registo sucede ao estado «por analisar» abaixo.
**Candidata:** o botão real de impressão foi aberto no novo dossier QA `d279172b-8bd1-4074-b000-ee25f0d29778`, oportunidade `309`, Manica. A saída revelou duas falhas delimitadas: estado de etapa exposto como `not_started` e ausência do detalhe dos requisitos específicos. Corrigidas na composição do relatório: estado humano existente, requisitos só da oportunidade seleccionada, com documentos/fundamento, e exclusão dos ramos dispensados. Novo teste exclusivo `native-pdf-scope.test.mjs` passou; TypeScript e build passaram. Não se voltou a testar Word/ZIP. A revisão **13**, aberta pelo botão do portal após a correcção, apresenta `Por iniciar`, os cinco requisitos da segunda oportunidade e os pressupostos QA **123456 MZN**, **45678 MZN**, diferença **77 778,00 MZN**. A primeira oportunidade/Sofala não integra esse âmbito. Estes números são uma fixture sintética para conferência da cópia, não nova aceitação dos cálculos.
**PDF guardado ainda pendente:** o controlo do navegador permite abrir e ler a saída, mas não operar o diálogo nativo do sistema. Foi pedida a gravação manual da janela já preparada em `outputs/multi-opportunity-fix-20261023/candidata-1afdfee8.pdf`. Até ler/renderizar o ficheiro e conferir paginação, requisitos, âmbito e valores, a candidata continua exclusivamente QA; não houve push de disponibilização. `origin/main` foi consultado por `git fetch`: continua `55136b44`, sem alterações concorrentes por integrar nessa consulta. Preparar o avanço normal, sem force, e voltar a consultar antes de integrar. Não é necessária nova autorização de disponibilização; a ordem já foi expressa pela Tempo, condicionada à conferência do PDF.
### Projectos âncora — análise concluída e correcções/verificações delimitadas
Reutilizados os registos de reposição (`7c1f5321`, recibo de preparação adaptativa: 29 fichas/15 projectos, IDs/títulos preservados) e de continuidade carteira/oportunidade (`InvestmentPortfolioContinuityTest`, instalado, sem repetir). **Não foi confirmada perda de fichas ou revogação da divulgação.** Revisão estática do código actual e inventário de leitura apenas na base QA isolada, sem escrita. O inventário QA encontrou 15 fichas documentais de projecto com **sector, localização e região em texto** em todas elas, mas sem `primary_territory_id`, ligações públicas em `project_locations` ou oportunidades públicas directamente associadas. Uma décima sexta ficha marcada `SYNTHETIC QA — associação escolhida`, com uma oportunidade, foi identificada e excluída da contagem documental. A diferença QA não é atribuída à homologação; qualquer reposição de dados depende de comparação com o último estado demonstrado e identidades de acesso equivalentes, conforme AGENTS.
| Trecho | Encontrado / classificação | Correcção ou verificação finita |
| --- | --- | --- |
| Pesquisa por sector e localização | **Lacuna de implementação do filtro sectorial.** `InvestmentPortfolioController::index` e `portfolio/index` aceitam pesquisa textual, tipo e hierarquia territorial; não recebem nem apresentam sector. Texto livre contendo um sector não equivale ao filtro combinado. A pesquisa territorial já tem implementação por IDs e relações documentadas em `InvestmentGeography::constrainDossiersToTerritories`. | Acrescentar filtro sectorial e combiná-lo com a localização no mesmo catálogo, conservando filtros ao abrir/regressar. Reutilizar o catálogo de sectores; uma âncora pode abranger vários sectores documentados. Não classificar por semelhança de palavras nem criar ofertas para obter resultados. |
| Correspondência dos dados documentais | **Verificação de dados/ambiente em falta**, distinta da ausência do filtro. Os 15 sectores/localizações existem nos textos QA; o schema de `projects` não tem `sector_id` e os campos/relações territoriais consultados estão vazios nesse ambiente. | Comparar schema, dados, configuração e acesso equivalente com o último estado demonstrado/destino. Reutilizar fonte, exemplar e correspondências já aceites; delimitar as correspondências canónicas em falta e ambiguidades. Não pedir outra validação genérica da fonte, inventar território, nem declarar regressão a partir do clone. |
| Ficha → manifestação | **Mecanismo existente a preservar:** o botão cria rascunho com o mesmo `dossier_id`; `InvestmentSubmissionController::create` conserva `project_ids`. A ficha recupera os rascunhos existentes. **Sem nova prova de execução deste ramo nesta análise.** Para anónimos, o componente apresenta ligação simples a `/login`; a retoma com a identidade/filtros requer prova dirigida. | Conferir apenas login/retoma e recuperação do interesse na mesma ficha, sem submissão externa. Não transformar a ficha documental em oportunidade nem alterar os estados de publicação. |
| Ficha sem oportunidade → preparação → simulador | **Integração directa ainda não implementada.** A ficha mostra oportunidades relacionadas quando existirem; essas usam `from_dossier`, território e `portfolio_origins` já aceites até ao simulador. Sem oferta associada não existe acção de ideia baseada na própria âncora, e `InvestorContext::reference` não suporta uma origem de carteira autónoma. O vínculo explícito manifestação/dossier já existe, mas não cria essa origem no contexto. | Reutilizar o arranque por ideia e o mesmo dossier, acrescentando a referência documental da âncora independente de uma oferta. Conservar identidade, versão, fonte e localização escolhida até ao simulador/relatório e à manifestação vinculada. Conferir este ramo novo e a transição interesse/preparação; reutilizar as provas aceites do ramo com oportunidade. |
O recorte âncora deixou de estar «por analisar»: há **duas lacunas de implementação** (filtro de sector e entrada directa por ideia de âncora sem oferta), **uma reconciliação de dados/ambiente** e **uma prova dirigida de interesse/autenticação**, com a continuidade do ramo novo incluída no seu critério de aceitação. Não se implementou esse recorte nesta análise nem se declarou homologação corrigida. As lacunas de implementação não são regressões demonstradas. Fontes locais: `InvestmentPortfolioController`, `InvestmentGeography`, `InvestmentRelations`, `InvestmentOpportunityChoices::portfolioOrigins`, `InvestorContext`, `InvestmentSubmissionController`, `portfolio/index`, `portfolio/show`, `search-territory`; inventário QA em `outputs/multi-opportunity-fix-20261023/anchor-analysis-data.json`. Actualização nas mesmas linhas `AUD-REL-02`/`AUD-LEG-06` da matriz existente, sem nova matriz/backlog.
## Oportunidade em tratamento — correcção conjunta do saldo em QA, 23/10/2026
Origem: pedido expresso da Tempo após `bc84fbdb`. Corrigir conjuntamente os três consumidores da escolha de oportunidade; conferir os quatro recortes delimitados, corrigindo apenas falhas encontradas. **Só QA; não entregue na homologação nesta passagem.** Permanecem encerrados `6b544548`, `d8224ba8` e `55136b44` com os respectivos recibos; não se repetiram os seus testes. A lista histórica de sete itens abaixo passa a ter os seguintes estados correntes: Correc??o funcional `1afdfee8`; [recibo QA](investment-treatment-scope-qa-receipt-20261023.json).
| Item | Resultado corrente e prova dirigida |
| --- | --- |
| 1 — Simulador | Selector disponível na entrada geral; entrada da ficha/atalho transporta a chave; gravação e recuperação conservam a escolha e o snapshot, e a devolução financeira identifica a oportunidade. A troca aguarda a recuperação do contexto antes de gravar a nova associação. Provas API novas e conferência Edge: segunda/Manica → primeira/Sofala → segunda, no novo dossier QA. Valores deliberados continuam sujeitos à reconciliação existente. |
| 2 — Etapas/requisitos | Decisões, motivos e estados específicos conservados por chave da oportunidade no payload cifrado existente; dados e decisões comuns anteriores preservados. Numa selecção múltipla, uma dispensa comum não dispensa automaticamente todas as fichas. Primeira sem necessidade de terra e segunda com necessidade produzem etapas/acções distintas. Os links dos requisitos conservam a oportunidade; a próxima acção e o snapshot seguem o âmbito escolhido. |
| 3 — Terra/DUAT | Atalho procura o pedido do titular com o mesmo dossier **e a mesma oportunidade**, não o último de outra ficha. Teste cria dois pedidos pela ordem inversa e recupera cada associação. Sem escolha numa selecção múltipla, abre preparação com escolha, sem abrir arbitrariamente um pedido existente. |
| 4 — Retirar a primeira | Prévia, confirmação e recuperação por HTTP dirigidas: segunda ficha exactamente preservada, título comum deliberado conservado e pedido da primeira continua consultável como histórico. Não constitui nova avaliação visual integral do editor. |
| 5 — Dados comuns | Detectada duplicação de contactos já autorizados no payload do dossier; proposta de email/telefone ao simulador e Terra e nome a Terra corrigida. Testes verificam proposta e conservação de contacto deliberado. Perfil/empresa e organização mantêm os mecanismos próprios aceites; não se escolhe empresa/representante automaticamente, nem se converte país de residência/constituição em residência fiscal ou território do projecto. |
| 6 — Entradas alternativas | Teste novo de ideia livre e fontes explícitas sector/território: confirmação e recuperação no mesmo dossier e propostas territoriais em Terra/simulador. Teste de login real do cliente HTTP conserva o URL pretendido e a fonte sectorial. Não se repetiu a prova territorial de reautenticação já aceite, nem carteira/oportunidade. Estas provas HTTP não são apresentadas como nova avaliação visual de todas as entradas. |
| 7 — Exportações | Word e ZIP efectivamente gerados e lidos: identidade da segunda oportunidade e parque correctos, sem Sofala; ZIP contém o Word idêntico. Saída HTML do percurso de impressão contém o mesmo âmbito e distingue etapas aplicáveis/dispensadas. **Permanece por conferir apenas o novo PDF nativo efectivamente guardado pelo navegador**, incluindo paginação/legibilidade; HTML não substitui essa prova. Nenhuma falha do PDF foi presumida. |
Verificações novas: `InvestmentTreatmentScopeTest` (sete casos dirigidos, 170 asser??es; guarda efectiva da base `simulator_fiscal_delta_20261021_test`, porta 55441; transacções), `treatment-scope-export.test.mjs` (dois casos; artefactos reais Word/ZIP e composição de impressão), TypeScript/build. Os erros de preparação dos ensaios (nome de campo de versão no teste e confirmação territorial do clone QA) foram corrigidos, sem alterar a funcionalidade aceite. Fixtures e artefactos permanecem em `outputs/multi-opportunity-fix-20261023/`; novo dossier `d279172b-8bd1-4074-b000-ee25f0d29778`, separado dos dossiers aceites. Sem migrations, fixtures publicadas, alterações em dados activos, permissões, Estatísticas ou IA.
### Projectos âncora por sector/localização — recorte por analisar
**Incluído por pedido expresso da Tempo nesta passagem.** Verificar pesquisa combinada por sector e localização, abertura pela identidade existente, continuidade para manifestação de interesse e preparação do investimento, e recuperação no mesmo dossier com fontes/associações documentadas. A reposição/consulta anteriormente aceite de fichas e relações não demonstra, por si só, este requisito completo. **Estado: por analisar; não se presume implementação nem falha.** Registado em `AUD-REL-02` e articulado com `AUD-LEG-06`, sem nova matriz/backlog. Este recorte permanece no saldo antes da avaliação final pela Tempo; Estatísticas e IA continuam adiadas.
O saldo corrente separa, portanto, a correcção executável QA dos três pontos, as provas dirigidas com os limites acima, a conferência restante do PDF nativo e o novo recorte âncora por analisar. Não há fecho global nem declaração de entrega desta candidata na homologação.
## Saldo finito da aplicação transversal — 23/10/2026
Pedido expresso da Tempo: substituir a qualificação genérica de cobertura parcial por pendências delimitadas, sem repetir provas aceites. Este é o inventário corrente dentro do manual e da matriz existentes; as avaliações anteriores abaixo ficam como histórico, subordinadas aos fechos posteriores. Base revista: `b6a626c9`, com código entregue em `55136b44`. Revisão de código, contratos, testes existentes e recibos; **não houve execução funcional nova**, alteração de dados ou instalação nesta passagem. Uma lacuna de código abaixo não é apresentada como novo incidente observado no navegador.
### Corrigido, demonstrado e entregue — manter encerrado
| Recorte | Entrega / prova reutilizada |
| --- | --- |
| Gravação/idioma, origem dos dados no relatório, sete ligações da Beira | `6b544548`; recibo `4098192a`. |
| Complementos com identidade, necessidade sem oferta, serviço e relação à oportunidade no dossier | `d8224ba8`; recibo `af926180`; serviço sintético exclusivamente QA, Fiação em preparação/localização por definir. |
| Zonas/parques, Terra/DUAT, Balcão e manifestação: tratar a oportunidade escolhida, alterar só a segunda e conservar a primeira, guardar/recuperar associação e relatórios cobertos | `55136b44`; recibo `b6a626c9`; QA `4a01e9e2`. Inclui escolha explícita quando há várias, edições deliberadas e associação bloqueada de pedido devolvido. |
| Resumos editáveis no simulador, nomes territoriais, terreno Sim/Não/Por definir, ocultação/reabertura de etapa global e continuidade carteira/dossier | Incrementos `7ca94967`, `f2150b19`, `5bda74ae`, entregues na linha `7c1f5321` e preservados pelas entregas seguintes; recibos de preparação adaptativa e continuidade existentes. Não voltar a exigir a instalação indicada como pendente em notas históricas. |
### Pendências actuais — sete recortes, sem expansão implícita
| N.º / situação | Ecrã ou consumidor e limite exacto | Resultado necessário / próxima acção delimitada |
| --- | --- | --- |
| 1 — Falha de integração identificada no código | **Simulador iniciado pelo dossier com várias oportunidades.** `InvestorContext::prefill()` só delimita o simulador quando recebe a chave; `DossierPrefill` exclui o simulador do selector de oportunidade. A entrada geral continua a propor os campos comuns derivados da primeira, embora mostre todas as fichas. | Permitir identificar explicitamente o âmbito da análise e recuperar a oportunidade em tratamento; tornar explícito o âmbito comum quando for essa a escolha. Manter rascunhos, edições e relatórios aceites. Não somar investimentos nem inventar uma regra de agregação financeira. Prova nova apenas: seleccionar a segunda neste consumidor e conferir a sua localização/sector/parque e a identificação do âmbito no resultado. |
| 2 — Falha de integração identificada no código | **Dossier, etapas e próxima acção.** `InvestorPath::view()` calcula aplicabilidade por `investor_path.facts` global; `land_needed=no` oculta a acção Terra para o dossier todo. `InvestorContext::landPreparation()` também lê esse único facto. Não há distinção de decisões de terra diferentes entre duas oportunidades. | Conservar a resposta global válida, mas impedir que dispense silenciosamente a necessidade específica de outra oportunidade. Ligar decisão, explicação e próxima acção ao âmbito tratado; desconhecido continua pendente. Prova nova: uma oportunidade sem necessidade e outra com necessidade, sem repetir o ciclo global Sim/Não já aceite. |
| 3 — Falha de integração identificada no código | **Atalho Terra/DUAT no resumo do dossier.** `InvestorPath::view()` procura o caso mais recentemente actualizado do titular com o mesmo dossier, sem filtrar `context_opportunity`; o atalho abre esse caso. A correcção dos formulários não torna esta escolha do resumo específica. | Com dois pedidos de oportunidades diferentes, apresentar escolha identificável ou atalho por oportunidade, recuperando o pedido correspondente. Preservar os pedidos e respectivos snapshots; não substituir pelo último pedido do dossier. A lista de pedidos/resultados deve permitir distinguir as associações. |
| 4 — Ainda sem prova delimitada | **Editor de contexto: retirar, substituir ou reordenar a primeira oportunidade.** A prova aceite altera apenas a segunda; o código ainda projecta a primeira em `selection`/campos comuns. Testes de mudança territorial e de conservação de várias fichas são reutilizados, mas não demonstram este impacto na interface. | Conferir apenas a prévia da retirada/substituição da primeira, confirmação e recuperação: campos comuns propostos, edições preservadas, segunda intacta e pedidos antigos mantidos como históricos. Não declarar perda nem pedir nova prova da alteração exclusiva da segunda. |
| 5 — Ainda sem prova delimitada | **Reutilização perfil/empresa/organização → contexto e ferramentas.** A manifestação já recebe conta e empresa em `InvestmentSubmissionController::create()`; o perfil já existe. Isso não comprova a herança dos dados equivalentes na preparação do dossier, Terra e simulador. | Traçar apenas campos equivalentes e autorizados (identificação/contacto, representação, país/origem quando semanticamente coincidentes), registar o que chega, o que volta a ser pedido e a prioridade de edições locais. Corrigir só duplicação demonstrada. País de constituição, residência fiscal e localização do projecto não são intercambiáveis. Não repetir o preenchimento da manifestação já coberto. |
| 6 — Ainda sem prova delimitada | **Entradas por ideia, sector ou localização com passagem para operação privada.** Há prova de rascunho/reauth territorial em `InvestmentCorridorContextTest` e de carteira/oportunidade nos recibos aceites. Não é prova de todas estas entradas alternativas até ao consumidor. | Conferir somente cada entrada alternativa ainda não coberta: fonte e escolhas no arranque, regresso/autenticação e recuperação no mesmo dossier; pedir apenas dados ausentes. Reutilizar o caso territorial e as entradas carteira/oportunidade existentes, sem os repetir. Não pressupor que falham. |
| 7 — Ainda sem prova delimitada | **Exportações nativas do dossier/plano para âmbito múltiplo.** Valores, idioma, proveniência, exportações financeiras e recuperação já têm provas próprias; relatório Terra e associação canónica dos consumidores também. Falta isolar o efeito da escolha de âmbito nos restantes artefactos nativos, após resolver 1–3. | Conferir apenas correspondência oportunidade/localização/parque, etapas aplicáveis e identidade de pedidos em Word/PDF/ZIP efectivamente gerados. Não repetir cálculos, idioma, sete ligações, anexos nem o relatório Terra aceites. Um formato que não inclua esse conteúdo deve ser registado como sem aplicação, não como falha. |
São **três lacunas de integração constatadas por leitura e quatro verificações delimitadas**, não sete falhas de execução. Os itens 1–3 determinam a correcção executável seguinte; 4–6 podem ser conferidos independentemente em QA isolado; 7 depende apenas do âmbito corrigido. A contagem refere-se a estes sete recortes, não a cada formato ou campo. Uma verificação que passe encerra o item; uma que falhe regista o defeito concreto na mesma linha da matriz. Não criar outra matriz/backlog nem converter ausência de prova em regressão.
### Nunca implementado / desenvolvimento futuro — separado do saldo de regressões
O editor unificado de actores, fornecedores, contactos e infraestruturas das fichas territoriais e a cartografia de linhas de corredores não existem no produto demonstrado. Candidaturas com procedimento autónomo, restauro automático de snapshots e entrega a CRM remoto sem conector configurado também não são funcionalidades perdidas. Permanecem nos respectivos requisitos de desenvolvimento, fora dos sete recortes acima; não são pré-condições para os corrigir. Ausência de oferta ou associação documental não autoriza criar uma relação, localização ou publicação. Estatísticas e IA continuam adiadas por decisão da Tempo.
### Rastreabilidade e limite desta revisão
Consumidores revistos: editor/arranques de contexto, resumo e etapas do dossier, simulador e reutilização, parques, Terra/DUAT, Balcão, manifestação, perfil/empresas e composição dos relatórios. Fontes: `InvestorContext`, `InvestorPath`, `InvestmentConsumerScope`, `InvestmentSubmissionController`, `dossier-prefill`, `investment-opportunity-choices`, `project-dossier`, testes `InvestmentCorridorContextTest`, `InvestmentConsumerScopeTest`, `InvestorPathTest` e `context-inheritance`, recibos de instalação existentes. Não se auditou novamente gestão editorial, publicação, RLS, legislação ou cálculos fiscais; mantêm os seus inventários e aceitações próprios. A avaliação final de navegação e apresentação pela Tempo vem depois do fecho desta lista. Referências na matriz existente: `AUD-LEG-06`, `AUD-SIM-INT03`, `AUD-SIM-INT06`.
## Oportunidade em tratamento — entregue na homologação, 23/10/2026
Por instrução expressa da Tempo, a correcção QA `4a01e9e2` e o recibo `55136b44` foram integrados no `main` existente por avanço directo, sem force ou conflitos. O destino serve **`55136b44`**, confirmado em `https://teste-investidor.tempo.co.mz/build/revision.json`, build `2026-10-23T15:55:10.921100+00:00`, manifest SHA-256 `b2dd6172516f220344ede5fc9d7876b7fc8417416d03842d737452709379a12c`. [Recibo da instalação](multi-opportunity-context-installation-receipt-20261023.json).
Conferido apenas o delta instalado, com sessão normal já autenticada: a ficha real **Florestas — Corredor da Beira**, referência `INV-MZ-GEN-8326396601945337`, ID **53**, apresenta as ligações para Terra/DUAT e Balcão com `opportunity:53` e o dossier existente `958df138-7980-4685-8389-fa90be02ed6b`. GET autorizado do contexto com essa identidade devolveu apenas essa ficha no snapshot específico e Sofala nas propostas de Terra/DUAT, Balcão e manifestação. Não se criou, guardou ou submeteu qualquer pedido para esta conferência. Provas locais: `outputs/opportunity-consumer-install-20261023/`, com revisão pública, ligações, resumo da resposta e imagem; hashes no recibo.
As provas QA aceites dos quatro consumidores foram reutilizadas, sem repetir o ensaio da segunda oportunidade. O estado «concluído em QA, ainda não instalado» dos registos históricos abaixo é superado neste recorte pela revisão servida. O histórico de `d8224ba8` e das alterações concorrentes permanece ancestral da entrega; os componentes nucleares dos complementos e o recibo dos fechos anteriores coincidem por blob Git. Sem delta de migrations, configuração ou dependências, nem transporte de dados/fixtures; as oportunidades e serviços reais mantêm os estados de publicação existentes. A disponibilização usou o instalador automático existente, sem edição manual da pasta servida.
**Saldo:** zonas/parques, Terra/DUAT, Balcão e manifestação deste recorte estão agora **entregues na homologação**, com o delta instalado conferido. Complementos `d8224ba8` e fechos `6b544548`/`4098192a` preservados. Estatísticas e IA permanecem adiadas; não há alterações nesses módulos/configuração. A aplicação transversal fora deste recorte permanece parcial, sem declarar aceitação global nova. Actualizadas apenas as evidências de AUD-LEG-06 e AUD-SIM-INT03 na matriz existente.
## Oportunidade em tratamento — correcção executável QA dos quatro trechos, 23/10/2026
Pedido expresso da Tempo: corrigir as falhas confirmadas em `ad4160f0`, sobre a versão actual e preservando os complementos entregues em `d8224ba8`. Implementação `4a01e9e2` na branch `codex/opportunity-consumer-scope-20261023`. [Recibo QA](multi-opportunity-context-qa-receipt-20261023.json). Esta passagem supera o estado «não corrigido» do diagnóstico apenas nos quatro trechos aqui demonstrados; o diagnóstico histórico permanece preservado.
Cada oportunidade guarda agora a sua própria referência de zona/parque. A consulta e a compatibilidade usam o corredor e o território dessa oportunidade; alterar a segunda actualiza apenas as suas opções. O parque comum de registos anteriores é recuperado na primeira identidade a que pertencia. O rascunho permite conservar uma localização ainda por escolher após a mudança de corredor, sem repor silenciosamente a localização documental incompatível.
Terra/DUAT, Balcão e manifestação resolvem a oportunidade em tratamento pela identidade dentro do dossier autorizado. A entrada identificada na ficha/ligação transporta essa identidade; a manifestação mantém a identidade da ficha de origem ao associar o dossier. Sem escolha e com várias oportunidades, a interface pede uma selecção clara e propõe dados comuns, sem adoptar o território da primeira. Os links por oportunidade no dossier e na ficha dão entrada em Terra/DUAT e no Balcão. A reconciliação existente conserva edições deliberadas e detalhes locais das parcelas; os limites dos pedidos devolvidos no Balcão mantêm-se. O resumo geral de um dossier múltiplo deixa de apresentar a primeira como se fosse necessariamente a oportunidade em tratamento.
O servidor guarda no payload cifrado existente de cada pedido uma associação canónica: oportunidade, corredor, localização, parque, origem/versão e requisitos dessa oportunidade. Um snapshot enviado pelo cliente não substitui a origem autorizada. A manifestação conserva também a coluna de oportunidade coerente com a escolha e a associação nas versões/intake. O relatório imprimível de Terra/DUAT apresenta a associação guardada; o recibo do Balcão apresenta o contexto congelado; o modelo comum do relatório conserva cada parque junto da respectiva identidade.
| Prova dirigida | Resultado |
| --- | --- |
| Alterar apenas a segunda | Novo dossier sintético QA `1017980b-a61c-44a4-b8d5-bd909c2d1316`: segunda `309`, Gaza/Limpopo/parque QA Gaza → Manica/Beira/parque QA Manica pela interface, com motivo, confirmação, gravação e recuperação. Primeira `286` continua em Sofala/parque QA Sofala, exactamente igual à sua estrutura inicial por comparação integral. |
| Terra/DUAT | Entrada pela ligação da segunda, gravação e recuperação do pedido `01a16967-f058-7353-a7bc-33839b3c6595`. Identidade 309, Manica, parque Manica e requisitos próprios recuperados. Relatório HTML imprimível aberto e conferido visualmente com a mesma associação. |
| Balcão | Entrada com a segunda identificada; pedido `c3de8e43-701b-46bd-a016-64adc32e2394` preparado, guardado e recarregado. Mesma identidade, Manica, parque e requisitos. O template de recibo foi verificado em teste transaccional, sem inventar uma submissão real. |
| Manifestação | Preparada a partir da ficha existente da segunda; dossier associado pela interface. Pedido `01a16968-e3d2-728a-b28a-2fb05540509e` guardado e recarregado com 309/Manica/parque/requisitos. A versão guardada conserva o vínculo. Não houve submissão externa. |
| Várias sem escolha | Entrada Terra/DUAT apenas com o dossier apresenta a escolha de oportunidade, sem localização da primeira. Selecção explícita da segunda recupera Manica, o seu parque e os seus requisitos. |
Ambiente: Edge, `127.0.0.1:18574`, actor sintético normal QA 509 e guarda efectiva de `simulator_fiscal_delta_20261021_test:55441`. Reutilizadas as oportunidades QA existentes; preparados **três parques expressamente SYNTHETIC QA**, só na base isolada, para conferir opções positivas e incompatibilidade. Nenhuma oferta, oportunidade, serviço ou publicação real foi criado por essa preparação. O dossier aceite dos complementos conserva as três oportunidades/localizações e quatro relações, sem repetir os seus casos funcionais.
Validação final: **4 testes de integração, 104 asserções; 3 testes frontend dirigidos; TypeScript e build aprovados**. Cobertos também identidade alheia/actor alheio, snapshot forjado, conservação de edições deliberadas, parcela, rascunho territorial incompleto e associação bloqueada num pedido devolvido. Provas: `outputs/multi-opportunity-fix-20261023/verified-state.json`, textos recuperados e `land-report.png`; hashes no recibo. O ensaio do relatório é HTML imprimível, sem alegar nova aceitação de PDF nativo.
**Disponibilidade:** correcção executável e demonstrada em QA; `main` e homologação continuam em `d8224ba8`. Sem migration, importação ou escrita activa. Não se transportam as fixtures. Complementos e fechos `6b544548`/`4098192a` preservados. A aplicação transversal fora deste recorte continua parcial; não se declara fecho global nem aceitação organizacional nova. Actualizadas apenas as evidências de AUD-LEG-06 e AUD-SIM-INT03 na matriz existente.
## Contexto específico por oportunidade — conferência delimitada, 23/10/2026
Pedido da Tempo: complementos entregues em `d8224ba8`, sem pendência de disponibilização nem repetição dos seus casos ou dos fechos `6b544548`/`4098192a`. Conferida a alteração apenas da segunda oportunidade nos percursos ainda parciais. [Recibo técnico](multi-opportunity-context-review-20261023.json).
Reutilizado o dossier sintético QA existente `6fdbb963-726e-4048-9884-8cddc04e77a6`, actor normal QA 509, base guardada `simulator_fiscal_delta_20261021_test:55441`. Prévia, confirmação e recuperação pelos serviços existentes, dentro de transacção revertida: segunda oportunidade `309`, Gaza/Limpopo → Manica/Beira, com confirmação e motivo explícitos. Primeira e terceira escolhas conservadas exactamente. A versão interna passou 2 → 3; após rollback o registo original de `desk_cases` coincidiu integralmente com o anterior. Não se alterou a homologação nem se reavaliaram complementos.
| Percurso | Resultado e causa delimitada |
| --- | --- |
| Dossier | A alteração da segunda foi recuperada separadamente; a primeira continuou Sofala e a terceira Gaza. Não há perda geral das localizações neste ensaio. |
| Zonas/parques | Consulta e dependências do efeito de `context.tsx` usam apenas `choices[0]`; existe só `selection.park`, validado pelo servidor contra a primeira escolha. Alterar a segunda não muda os parâmetros dessa consulta nem permite atribuir-lhe parque próprio. Os conjuntos QA consultados estavam vazios: não se declara disponibilidade positiva nem eliminação de um parque. |
| Terra/DUAT | Antes/depois, a proposta continua uma parcela de Sofala, sem identidade da oportunidade. `prefill()` deriva-a dos campos comuns; o formulário aplica só `v[0]` à primeira parcela. Conferência visual de leitura mostrou as referências comuns e os complementos existentes, sem lista das localizações de cada oportunidade. |
| Balcão | Território e descrição continuam os da primeira escolha. A diferença executada é o texto da revisão v2 → v3 na origem territorial; isso não transporta o âmbito da segunda oportunidade. |
| Manifestação | Sector, objectivo e localização propostos permanecem iguais, incluindo Sofala. A proposta não identifica a oportunidade a que cada âmbito se destina. |
**Saldo:** causas confirmadas; estes quatro trechos não ficam fechados nem corrigidos por esta avaliação. O componente comum só apresenta `OpportunityChoices` no simulador. A correcção deve identificar expressamente a oportunidade destinatária das zonas/parques e dos dados propostos em cada formulário, preservando as restantes escolhas e edições deliberadas, e demonstrar guardar/recuperar após mudar apenas a segunda. A confirmação/recuperação deste ensaio foi nos serviços da aplicação, não nova sessão de navegador nem submissão dos três formulários. Evidência local: `outputs/multi-opportunity-review-20261023/probe.json` e `land-visible.txt`. Sem nova matriz/backlog; actualizada apenas a evidência de AUD-LEG-06 e AUD-SIM-INT03 na matriz existente. A aplicação transversal permanece parcial.
## Complementos — disponibilização em homologação, 23/10/2026
Por decisão expressa da Tempo, o recorte demonstrado foi integrado pelo Git existente e disponibilizado em https://teste-investidor.tempo.co.mz, revisão `d8224ba8`. A integração `088f2c2c` preservou as alterações concorrentes de `43cd0be7`; `d8224ba8` actualizou apenas o teste da entrada pública do simulador à estrutura da página inicial integrada. O instalador tinha recusado a referência ao componente antigo, também falhada no main concorrente, conservando a release anterior. Não se desactivou a verificação.
O recibo existente regista a revisão efectivamente servida, a instalação e a conferência delimitada do delta. Mantêm-se as provas QA aceites, sem nova ronda dos mesmos casos. Não houve migrations, importações nem transporte dos exemplos sintéticos. As oportunidades e serviços reais conservam identidades e estados de publicação. Fiação permanece necessidade em preparação, sem localização ou oferta inventadas.
Os estados «homologação inalterada» e «não instalado» nas passagens históricas abaixo descrevem essas passagens e são superados apenas neste recorte. As pendências de contextos específicos em zonas/parques, Terra/DUAT, Balcão e manifestação continuam abertas; a aplicação transversal permanece parcial. Fechos `6b544548`/`4098192a` preservados.
## Complementos — conferência visual QA concluída no recorte pedido, 23/10/2026
Pedido da Tempo: conferir abertura da ficha complementar existente, preparação de necessidade sem oferta identificada, selecção de serviço/instrumento e recuperação de selecção/origem/localização; com várias oportunidades, verificar a ligação à oportunidade escolhida, sem assumir a primeira. Homologação inalterada e fechos anteriores preservados. Esta passagem supera **apenas a pendência visual dos complementos neste recorte** nas secções abaixo; não declara a aplicação transversal concluída.
Ambiente usado: navegador Edge em `http://127.0.0.1:18574`, servido pelo router QA existente, com guarda efectiva de `simulator_fiscal_delta_20261021_test:55441` em cada pedido. Reutilizada a sessão normal da conta sintética QA 509; sem criar sessão, contornar MFA ou conceder papéis. Criado pela interface o dossier **SYNTHETIC QA — conferência visual de complementos 23-10-2026**, ID `6fdbb963-726e-4048-9884-8cddc04e77a6`; nenhum dossier anterior foi editado.
| Passagem visual | Resultado demonstrado |
| --- | --- |
| Abrir oportunidade complementar existente | Silos/armazenagem → Milho, trigo e arroz — Corredor da Beira, ID **286**, referência e slug `INV-MZ-GEN-8128691124566423`/`inv-mz-gen-8128691124566423`, conservados. A entrada de preparação recebeu a relação à carteira e a mesma identidade, sem reimportação. |
| Preparar necessidade sem oferta identificada | Acção de Fiação abriu a preparação no mesmo dossier e apresentou **Necessidade complementar — ideia em preparação**. Confirmada e recuperada com origem no Corredor da Beira e Centro. A localização inicialmente não escolhida ficou **Por definir**, sem receber Sofala por associação à primeira oportunidade. |
| Seleccionar serviço associado | Ficha do serviço QA **17**, de Formação, aberta e seleccionada por **Seleccionar este complemento e continuar no dossier de origem**. Origem: segunda oportunidade QA **309**; localização: **Gaza**. Recuperadas após confirmação e recarregamento. |
| Várias oportunidades, sem assumir a primeira | Primeira ficha **286 → Sofala**; segunda **309 → Gaza**. O complemento de frio **310** e Formação **17** ficaram ligados à **309**, em **Gaza**; a primeira ficha continuou em Sofala. A consulta das fichas e o retorno conservaram as escolhas pendentes, incluindo Fiação. |
| Recuperar no dossier | Após **Rever dados antes de guardar → Guardar dados do investimento → Voltar ao dossier → recarregar**, recuperaram-se três fichas e quatro relações complementares: Silos/armazenagem, Fiação, Formação e Cadeia de frio, cada uma com a sua origem e localização informada ou indeterminada. O título deliberado do dossier permaneceu intacto. |
**Âmbito dos exemplos:** a ficha 286 e a origem da carteira são identidades já existentes em QA. A base QA não tinha ofertas de serviços; para a prova visual positiva foram preparados **dois projectos, duas oportunidades (309/310), uma relação complementar pública e um serviço (17)**, todos explicitamente marcados **SYNTHETIC QA**. O serviço foi tornado visível unicamente como fixture da base QA guardada, não como publicação real nem aprovação da Tempo; não foi transportado para a homologação. A prova não declara um serviço real disponível no destino. Os registos anteriores de oportunidades (45), projectos (17), fichas da carteira (30) e parcerias (zero) permaneceram iguais por hash na preparação das fixtures. Originais, relações, permissões e aceitações anteriores foram preservados.
Provas locais: [ficha existente](../../../outputs/complements-visual-20261023/existing-opportunity.png), [serviço e sua origem](../../../outputs/complements-visual-20261023/associated-service.png), [oportunidades recuperadas](../../../outputs/complements-visual-20261023/recovered-opportunities.png) e [complementos recuperados](../../../outputs/complements-visual-20261023/recovered-complements.png). Texto visível recuperado e recibo das fixtures no mesmo directório. Actualizado o [recibo existente](investment-complements-qa-receipt-20261023.json) e a cobertura das cinco linhas existentes da matriz. Não houve nova alteração funcional, migration, instalação, importação ou escrita na homologação nesta passagem; as suites automáticas anteriores não foram repetidas.
**Saldo preservado:** conferência visual positiva dos complementos descritos, incluindo origem na segunda oportunidade. Zonas/parques, Terra/DUAT, Balcão e manifestação com contextos específicos de várias oportunidades **continuam por demonstrar**. Não se declara aceitação da Tempo, disponibilização no destino ou fecho global. Os três fechos de `6b544548`/`4098192a` mantêm-se.
## Complementos ligados ao investimento e ao dossier — preparação QA, 23/10/2026
**Origem funcional:** pedido expresso da Tempo nesta retoma: ligar regadio, silos, frio, processamento, fiação e logística às oportunidades existentes; distinguir necessidades ainda sem oferta; ligar certificação, formação e financiamento aos serviços/instrumentos existentes, conservando contexto e identidade no dossier. Este incremento não reabre gravação/idioma, proveniência do relatório ou Beira → sete oportunidades, encerrados no âmbito de `6b544548`/`4098192a`. Mantêm-se as aceitações e os limites transversais anteriores.
**Causa delimitada no código anterior:** `content.focus` era uma string, validada com os restantes campos textuais em `InvestmentPortfolioController::save` e apresentada por `portfolio/show.tsx` como texto em `<dd>`. Não existia resolução desses complementos para identidades de oportunidades/serviços nem selecção complementar no contexto privado. O bloco separado de oportunidades relacionadas não conservava uma relação tipificada «origem → complemento escolhido». Isso demonstra a integração ausente nesse trecho, não eliminação de fichas ou perda de dados. A tabela `complementary_project_links` já existia; não se criou outra tabela ou cadastro paralelo.
**Comportamento preparado no percurso existente:**
- As fichas públicas da carteira e das oportunidades apresentam os complementos mencionados no seu contexto documental. O resolvedor `InvestmentComplements` cruza a menção com as relações existentes à carteira, as ligações públicas entre projectos complementares ou a cadeia de valor publicamente documentada. A menção isolada não associa oportunidades fora dessas relações. Mantêm-se os estados publicados da classificação, incluindo referências preliminares, sem os promover a confirmação.
- Uma oportunidade identificada abre pela mesma rota/ID/slug; a escolha inclui origem, tipo de complemento, ficha de destino, localização escolhida, versão, trecho, fonte e hash do contexto consultado. Quando a componente está expressamente incluída na própria oportunidade, liga a essa identidade e explica-o, sem criar outra ficha.
- Certificação/formação/financiamento consultam ofertas publicadas da Bolsa explicitamente ligadas à origem, distinguindo `offering` de `seeking`. Serviços ligados à ficha da carteira ou ao seu projecto/cluster também são reutilizados. Financiamento reutiliza o tipo financeiro existente; uma necessidade de financiamento no resumo de um projecto não é, por si, uma oferta. Não se activaram contratos modulares nem instrumentos por importação.
- Sem oferta identificada nas relações desse contexto, apresenta-se **necessidade complementar**, com acção para iniciar uma ideia ou prepará-la num dossier existente. Não se cria nem publica uma oportunidade como efeito dessa acção. A ausência de correspondência neste recorte não declara inexistência absoluta no acervo.
- No editor do dossier, os complementos são consultados por oportunidade e localização; um território conhecido incompatível exclui o destino, e localização desconhecida continua «por definir». As escolhas têm prévia e confirmação, rascunho, retirada explícita e recuperação. Ao consultar/seleccionar outro complemento, a navegação aguarda a gravação das edições pendentes; o contexto acompanha a consulta da ficha pública. Uma entrada antiga retirada não volta por actualização; uma nova selecção explícita pode recuperá-la.
- A relação fica no payload cifrado e no histórico existente do dossier, aparece no dossier e no componente comum dos quatro formulários, e acompanha o snapshot do simulador e as linhas do modelo comum de relatório/exportação. Os dados deliberados do investimento principal conservam-se. Uma oferta retirada não pode entrar como nova escolha; uma relação anteriormente confirmada conserva o snapshot para revisão, sem republicar a oferta. Uma origem indisponível na consulta em lote é assinalada e não oculta os restantes resultados.
**Escolhas técnicas, sem novas exigências organizacionais:** vocabulário literal dos nove âmbitos solicitados para identificar trechos; IDs e relações existentes para resolver destinos; filtragem pela hierarquia territorial pública; selecção complementar no JSON cifrado e confirmação/versionamento já existentes. Não houve migration, novo papel, nova aprovação, importação, publicação, alteração de dados activos ou instalação na homologação. Não se transportaram dados QA para o destino.
**Verificação delimitada:** testes novos de `InvestmentComplementsTest` na base isolada `simulator_fiscal_delta_20261021_test`, porta 55441, com guarda do alvo e transacções revertidas; testes frontend novos em `investment-complements.test.mjs`; TypeScript e build. A prova da cadeia de valor usa um contrato público sintético identificado como tal, sem declarar novas relações reais no cadastro. Recibo resumido: [investment-complements-qa-receipt-20261023.json](investment-complements-qa-receipt-20261023.json). Não se repetiram os três casos aceites. Não se declara conferência visual nova no navegador, instalação ou aceitação da Tempo.
**Guia de utilização preparado:** abrir a ficha principal → **Investimentos e serviços complementares** → consultar a ficha correspondente ou **Iniciar preparação de uma ideia** → seleccionar o dossier autorizado ou criar a ideia → rever os complementos, localizações e diferenças → confirmar no editor. Para retirar, usar **Retirar esta relação da preparação** e confirmar a comparação. Os formulários ligados mostram a relação consultada, mantendo os seus próprios dados e operações de gravação/submissão.
**Saldo:** este incremento cobre a ligação e conservação dos complementos resolvidos pelas fontes/relações acima. Não declara associação automática de todos os complementos a todas as fichas, cadastro completo dos serviços ou cobertura de todas as expressões possíveis. As opções comuns de zonas/parques e a passagem de requisitos específicos de várias oportunidades aos restantes formulários, identificadas na avaliação anterior, continuam parciais. Matriz existente: RF-CLU-003, RF-COR-004, AUD-REL-02, AUD-LEG-06 e AUD-SIM-INT06, sem nova matriz/backlog.
## Avaliação dos trechos restantes — 23/10/2026
Decisão expressa da Tempo nesta retoma: **encerradas no âmbito reportado** gravação/idioma, origem dos dados no relatório e ligação da Beira às sete oportunidades; entrega `6b544548`, recibo `4098192a`. Reutilizar essas provas, sem repetir os casos aceites. **A aplicação transversal permanece parcial.** Esta passagem é avaliação estática do código e inventário de cobertura existente; não é nova prova visual, execução dos testes citados ou alteração na homologação. Não foi confirmada perda adicional de dados ou funcionalidades.
| Prioridade | Implementação e provas existentes a preservar | Limite encontrado / trecho ainda a demonstrar |
| --- | --- | --- |
| Várias oportunidades | `InvestmentOpportunityChoices`, `InvestorContext` e `investment-opportunity-choices.tsx` conservam território, actividades e origens por ficha. O teste existente `test_multiple_opportunities_keep_sites_sources_requirements_and_results_in_one_dossier` cobre confirmação, recuperação, requisitos agregados e resultados preservados. Não foi repetido. | `InvestorContext.php:384–386` escolhe a primeira oportunidade para os campos comuns. Avaliar retirada/mudança da primeira e alteração apenas da segunda, sem atribuir os dados comuns a todas as fichas. A existência do teste não demonstra a interface deste delta. |
| Corredor/localização | Cada cartão tem corredor e caminho territorial próprios; a mudança conserva caminho compatível e retira níveis incompatíveis com explicação. O teste existente `test_corridor_change_keeps_compatible_paths_and_removes_only_incompatible_geography` preserva esta cobertura. | `context.tsx` carrega parques por `choices[0]`, e `InvestorContext` também usa a primeira escolha na compatibilidade comum. Uma mudança apenas na segunda não altera esses parâmetros. Falta demonstrar como se apresenta e distingue a aplicabilidade das zonas/parques nas várias oportunidades. Não inferir que todas perderam a localização. |
| Reutilização nos restantes formulários | `DossierPrefill` está integrado em simulador, Terra/DUAT, balcão e manifestação. `inheritCompatibleFields` preenche vazios, acompanha herança identificada e conserva valores deliberados; a interface permite comparar/substituir campos. Balcão respeita os campos editáveis de um pedido devolvido. | `prefill()` entrega uma parcela territorial comum a Terra/DUAT e valores comuns aos outros destinos. No componente de reutilização, a lista `OpportunityChoices` só aparece para `simulator`. Não está demonstrada a passagem dos contextos específicos de duas oportunidades aos restantes formulários; a ligação ao mesmo dossier não comprova essa cobertura. |
Próxima prova delimitada em QA isolado: duas oportunidades com corredores/localizações distintos; mudar apenas a segunda e conferir opções/requisitos e conservação da primeira; retirar a primeira e comparar o impacto proposto nos campos comuns; abrir Terra/DUAT, balcão e manifestação a partir desse dossier, conferir dados comuns e referências específicas, preservar uma edição deliberada e recuperar os formulários. Usar apenas as etapas aplicáveis, sem manifestação/submissão real. Não repetir idioma/gravação, proveniência do relatório ou as sete ligações aceites. A comparação entre primeira e segunda escolha é um método técnico de avaliação, não uma nova regra de negócio nem decisão de escolher uma localização comum pela Tempo.
Cobertura registada nas linhas existentes AUD-SIM-INT03 e AUD-LEG-06 da matriz; sem nova matriz ou backlog. Nenhuma alteração de código funcional, schema, dados, configuração ou permissões nesta avaliação. Os limites acima permanecem pendentes de demonstração/correcção delimitada; não são declarados resolvidos por documentação.
## Instalação e conferência das três correcções — 23/10/2026
**Revisão servida: `6b544548`.** Conferência visual QA da Beira concluída antes da integração: sete oportunidades visíveis, Florestas abre com origem da carteira conservada nos dois botões de preparação. Integrada por avanço directo no Git, sem alterações concorrentes em main. Executor concluído às 13:45:19 UTC, 206/206 testes frontend; zero migrations/importações e nenhuma alteração de permissões. Recibo: `continuity-corrections-installation-receipt-20261023.json`. As referências abaixo a QA pendente/homologação em `7c1f5321` descrevem o estado histórico anterior, sucedido por esta instalação.
Delta no destino, apenas no dossier privado já identificado como teste `958df138-7980-4685-8389-fa90be02ed6b`: seleccionar Português (Moçambique), Guardar e continuar mais tarde e recarregar recuperou o idioma, com confirmação e sem falso conflito. Relatório imprimível revisão 4 identifica título herdado dos dados introduzidos no dossier, resumo herdado da oportunidade Florestas v2, sector herdado do catálogo sectorial e Sofala do catálogo territorial; sem repetir o teste de todas as exportações/paginação. A guarda do servidor coincide por SHA-256 com a testada em QA; reutilizada a recusa de versões antigas, sem provocar escrita desactualizada em dados do destino.
Na ficha pública da Beira, as sete ligações foram abertas individualmente: Açúcar, Carne bovina, Florestas, Hortícolas, Milho/trigo/arroz, Sésamo e Soja. Títulos coincidentes e origem `from_dossier` conservada em todos os botões de preparação. Não foram publicadas novamente as 34 oportunidades nem alteradas relações por província. Provas e revisão anterior preservadas no recibo. Aplicação transversal continua parcial; este fecho respeita apenas os três deltas pedidos.
## Correcções da verificação de homologação — candidata QA, 23/10/2026
Referência: `c3f7c3ad`; pedido expresso da Tempo nas três prioridades. As 34 oportunidades já públicas e as provas aceites mantêm-se. Esta candidata não altera a homologação, que permanece em `7c1f5321`; sem migrations, novos papéis, publicação, importação ou alteração de dados activos.
1. **Gravação/idioma — causa de transporte reproduzida.** O Nginx do destino comprime JSON e enfraquece o ETag da representação; confirmado em leitura pública do questionário (o controlador produz uma versão forte e a resposta chega `W/"questionnaire-…"`, com gzip). O cliente do rascunho devolvia literalmente o cabeçalho ao servidor, que compara a versão forte exacta; os logs do dossier de teste mostram PUT 200 seguido de 412 e repetição após recuperação. Em QA isolado, um router de prova aplica gzip e enfraquece os ETags dos rascunhos para reproduzir esse transporte. A correcção normaliza apenas o formato conhecido `W/"sfd-UUID-vN"` antes de o usar; não aceita outra versão, não muda o controlo do servidor nem força retry/overwrite. No navegador QA, mudar o idioma para English, guardar e recarregar recuperou English com confirmação de gravação, título deliberado e escolhas preservados, sem falso conflito. A leitura pública comprova o transporte no destino; a correcção do rascunho ainda aguarda instalação/conferência desse delta na homologação. Sem consulta de credenciais ou bypass de sessão/RLS.
2. **Proveniência do relatório.** A origem existente por campo estava no dossier, mas não viajava no snapshot usado pelo relatório; as linhas tinham atribuições genéricas fixas ao utilizador. O snapshot passa a incluir apenas tipo/título/versão/revisão da proveniência, sem IDs de actores. O modelo comum de relatório/CSV usa também as assinaturas de herança já existentes: mostra origem na oportunidade/catálogo/dados do utilizador no dossier; uma alteração posterior no simulador é identificada com a origem anterior. Não muda valores nem reescreve relatórios emitidos; metadados ausentes de versões antigas não são inventados. Conferido no relatório QA: resumo herdado da oportunidade, título alterado posteriormente e referência da oportunidade/localização/associação conservadas. Captura `outputs/preservation-inventory-20261023/corrected-provenance-report-QA.png`.
3. **Beira → oportunidades.** A consulta da carteira só procurava pivots; o cadastro das 34 conserva a pertença documental no `workflow_context`, deliberadamente sem inferir relações com mestres. As fontes aceites distinguem COR-02 «Corredor da Beira e Centro» (`investment-portfolio.json`) e o grupo «Corredor da Beira» (`corridor-investment-opportunities.json`): sete entradas, não selecção por província. Esta correcção implementa uma referência documental delimitada entre essa ficha e esse grupo, sem renomear/fundir corredores, acrescentar pivots ou inferir implantação. A consulta exige a chave original e o hash da fonte aceite; outras oportunidades/corredores não entram por semelhança. A mesma resolução serve a ficha, a pesquisa por corredor, a validação da origem no detalhe e a associação escolhida no dossier. As sete fichas são Milho, trigo e arroz; Soja; Hortícolas; Sésamo; Açúcar; Florestas; Carne bovina — todas no Corredor da Beira. Não se estende esta correspondência aos outros corredores, clusters ou âncoras por inferência.
Validação delimitada: 26 testes frontend afectados passaram; novo teste de proveniência/ETag repetido após o ajuste de designações, dois aprovados. Três testes transaccionais de continuidade/Beira, 91 asserções, na base QA `simulator_fiscal_delta_20261021_test:55441`, incluindo sete resultados, pesquisa, detalhe/origem e recusa de outra oportunidade. Um teste adicional, cinco asserções, confirma que o servidor continua a recusar ETag fraco e versão antiga e aceita apenas a versão forte actual. Types/build aprovados. A conferência visual final da navegação da carteira QA foi interrompida pela automação do navegador; não se declara uma prova visual nova dessa ficha, coberta aqui pela resposta Inertia/API e validação de continuidade. Mantêm-se as provas anteriores. Matriz existente: evidências acrescidas em AUD-SIM-INT03/06/07, sem novo backlog. Aplicação transversal permanece parcial.
## Verificação do produto na homologação — 23/10/2026
Origem: pedido da Tempo de avaliar carteira → escolha → dossier → simulador → relatório, registando apenas diferenças e reutilizando as provas aceites. Revisão servida confirmada: `7c1f5321`. Sem alteração de código, permissões, originais ou publicações nesta passagem. Referências da matriz existente: AUD-SIM-INT03 (recuperação), AUD-SIM-INT06 (relatórios) e AUD-SIM-INT07 (gravação/erros).
Criado pela interface um dossier privado explicitamente denominado **TESTE DE HOMOLOGAÇÃO — continuidade 23-10-2026**, `958df138-7980-4685-8389-fa90be02ed6b`, rascunho do simulador `6c07a968-7ff9-42fa-b252-072bb1acf221`. Cenário sintético, não investimento real nem manifestação/submissão. Os dossiers anteriores permaneceram sem edição. A escolha Florestas — Corredor da Beira foi recebida; o corredor já vinha seleccionado e apresentou apenas Manica/Sofala. Escolhida Sofala e actividade florestal para o teste, a prévia propôs resumo/sector/localização e preservou o título deliberado do teste. Após confirmação, oportunidade, Agronegócio, Corredor da Beira e Sofala recuperados no dossier e no simulador sem preenchimento repetido. Necessidade de terra Não, identificada como cenário sintético: acção Terra/DUAT e etapa de regularização retiradas do dossier; Situação do terreno ocultada no simulador com explicação. Não se repete a prova de novo login já aceite em QA.
Diferenças observadas, sem declarar causa ou correcção não demonstradas:
- **Gravação/recuperação (AUD-SIM-INT03/07):** no novo rascunho e num único separador desta verificação, após escolher Simples/Português e navegar nas etapas surgiu «alterado noutro separador». Recuperar a versão mais recente conservou Simples e os dados herdados, mas não o idioma. Escolher novamente o idioma e Guardar e continuar mais tarde voltou a indicar gravação não confirmada/conflito. Não há outro separador deste dossier de teste; não atribuir a falha ao utilizador nem afirmar causa técnica confirmada. Não contornado o controlo de versão.
- **Origem no relatório (AUD-SIM-INT06):** o relatório imprimível contém o resumo e Agronegócio herdados, mas classifica-os como declarados/seleccionados pelo utilizador. A proveniência apresentada deve distinguir herança da oportunidade de edição deliberada. A selecção do território foi efectivamente feita no teste.
- **Carteira → escolha:** a ficha Corredor da Beira e Centro abre, mas informa não ter oportunidades publicadas associadas nem relações disponíveis; por essa ficha não foi possível demonstrar a passagem contextual à oportunidade. Florestas também apresenta ausência de relações publicadas. A falta de associações já estava identificada; esta observação localiza o limite no destino, não prova perda recente nem autoriza inferir/recriar ligações. O dossier antigo Florestas `90110be6-00c1-4e37-a82f-19e734f4e097` abriu sem oportunidade/sector/localização confirmados; o título não prova esses dados guardados. Foi preservado.
**Relatório:** a opção Imprimir / guardar como PDF gerou uma nova apresentação, lida no navegador. Inclui as onze secções seleccionadas, oportunidade/Sofala, necessidade de terra Não e preparação do terreno não aplicável, lacunas como Não informado e ausência de plano/cálculo sem zeros presumidos. Cabeçalho/tabelas legíveis na captura; não se declara nova conferência de paginação PDF ou de todas as páginas impressas. O relatório foi gerado com o aviso de gravação activo: não atesta que todas as últimas edições locais estavam persistidas. A tentativa de captura de download Word pela automação não terminou; isso não basta para declarar defeito do exportador nem download/abertura Word demonstrados. As provas QA desses formatos mantêm-se, sem substituir a lacuna observada de gravação.
**Correcção de saldo das 34 oportunidades:** a pendência de disponibilização não corresponde ao destino actual. O catálogo mostra as entradas; a exportação pública Agronegócio devolveu 34 referências, todas coincidentes por URL com `agribusiness-homologation-20261023.json`, zero ausentes. Não houve reimportação, nova publicação ou conferência individual de todas as páginas. Disponibilidade pública e associações à carteira são estados distintos.
Provas locais: `outputs/preservation-inventory-20261023/product-report-homologation-20261023.png` e `product-simulator-save-conflict-homologation-20261023.png`. Saldo: herança, localização e não aplicabilidade demonstradas neste cenário instalado; gravação/recuperação com falha nova; proveniência do relatório a corrigir; passagem por associações reais da carteira não demonstrada neste recorte. Aplicação transversal parcial. Nenhum fecho global do produto.
## Retoma e recuperação da continuidade — conferência QA, 23/10/2026
**Estado posterior: instalada na homologação.** A revisão `7c1f5321`, que inclui `6948ed62`, `3d877ec7`, `115c7731` e a correcção histórica do relatório, está efectivamente servida desde 23/10/2026 às 13:16 UTC. Recibo: `adaptive-portfolio-installation-receipt-20261023.json`. Conferidos revisão pública, assets do delta, hashes dos cinco ficheiros afectados e carteira anónima: 29 fichas, zero IDs ausentes e zero títulos divergentes. Executor: 204/204 testes frontend; zero migrations/importações e nenhuma alteração de permissões. Reutilizadas as provas privadas QA; não se gravou um dossier real no destino. As menções abaixo a homologação inalterada conservam o estado histórico da preparação QA, agora sucedido por este recibo. Aplicação transversal permanece parcial.
Disponibilização autorizada pela Tempo: `115c7731` integrado em `main` por avanço directo, sem alterações concorrentes no remoto, migrations ou alterações de dependências/permissões. A primeira execução automática preservou a release anterior `e1f010f9`: 203/204 testes frontend passaram; o teste do documento histórico não aplicável detectou que a simplificação do relatório ocultava também esse registo. Correcção delimitada: manter a linha histórica quando existe documento associado, sem voltar a exigir o requisito; os restantes não aplicáveis conservam a explicação breve. Verificados 17 testes afectados de relatórios, continuidade e terra, mais types. Não foram repetidas as provas funcionais QA nem alterados dados no destino. A integração Git não é recibo de instalação; confirmar a revisão servida antes de declarar disponível.
Ponto de retoma da Tempo: 29 fichas visíveis; completar a continuidade, sem repetir a reposição ou as provas aceites. A implementação `3d877ec7` sucede a `6948ed62` e permanece exclusivamente em QA. As secções históricas abaixo que identificam a perda de `from_dossier` descrevem o diagnóstico anterior: essa lacuna está corrigida neste recorte QA, ainda não na homologação.
Conferência pela interface, apenas com conta e registos SYNTHETIC QA: a oportunidade, Sofala e a associação escolhida chegaram ao dossier e ao simulador. Foi guardado no simulador o título deliberadamente diferente «SYNTHETIC QA — edição deliberada de continuidade». Regressar ao dossier e reabrir Plano e simulador conservou esse título; recarregar também o conservou. Depois de Terminar sessão e efectuar novo login normal, abriu-se o mesmo investimento pelo painel e o seu Plano e simulador: oportunidade, localização, associação e título deliberado foram recuperados, sem novo preenchimento. O título do dossier e o da oportunidade não foram substituídos pela edição do simulador. O login conduziu ao painel; a recuperação foi concluída pela ligação do investimento nesse painel, não se afirma redireccionamento automático ao simulador.
Prova local: `outputs/preservation-inventory-20261023/portfolio-continuity-recovery-login-QA.png`. O dossier sintético usado é `b5248bb8-5ada-445f-a34a-e4c628b6d457`; o rascunho recuperado é `b56a6bb8-c5df-4f22-84d0-07534d9dd8ba`. Nenhum dado real ou permissão foi alterado para esta conferência.
Relatórios: reutilizadas as provas aceites de `tests/frontend/investment-portfolio-continuity.test.mjs` para o modelo comum de relatório/CSV/impressão, e as provas de aplicabilidade e situação do terreno da entrega reconciliada. Não se declara uma nova exportação visual nesta passagem nem se repete a suite. A causa e prevenção da perda anterior permanecem na secção seguinte e no manifesto/recibo existentes. Homologação inalterada; aplicação transversal ainda parcial; associações ausentes não foram inventadas.
## Continuidade carteira → oportunidade → dossier → simulador — incremento QA, 23/10/2026
Origem: pedido expresso da Tempo de conservar oportunidade, localização e associações escolhidas no mesmo percurso, sem repetir perguntas ou sobrescrever edições deliberadas. Complementa a entrega QA `6948ed62`; não instala esse conjunto nem este incremento na homologação.
Os dois botões Preparar investimento da oportunidade passam a transportar a origem `from_dossier` já validada e os filtros territoriais escolhidos. A entrada mostra a oportunidade, localização proposta e associação da carteira antes de escolher/criar o dossier. As mesmas referências atravessam autenticação, rascunho por actor, recuperação, prévia, confirmação e a referência guardada do simulador. Permanecem no payload cifrado existente: sem migrations, papéis ou percurso paralelo. Posto/localidade não são inferidos quando faltam.
Cada oportunidade conserva as suas próprias associações escolhidas (`portfolio_origins`), com identificador, título, versão, ligação e hash documental. O servidor só aceita fichas publicadas da carteira com ligação cadastrada à oportunidade. Não cria relações por semelhança de nome, corredor ou proximidade e não publica fichas privadas. É possível retirar uma associação pela interface sem retirar a oportunidade. Uma mesma associação pode acompanhar oportunidades diferentes, mantendo cada identidade e localização.
Guia: abra uma oportunidade associada a partir da carteira e escolha Preparar investimento. Na entrada, confirme visualmente a oportunidade, localização recebida e associação; continue um dossier existente ou crie um novo. Reveja e guarde no editor existente. A confirmação retira da URL a intenção de entrada já consumida, para que actualizar recupere o contexto guardado, incluindo associações retiradas, em vez de reaplicar essa entrada. Abra Plano e simulador no mesmo dossier; os dados e referências continuam disponíveis, com os controlos existentes para editar divergências e exportar.
Ao retomar uma oportunidade já existente num dossier, acrescentam-se as associações recebidas sem substituir localização, corredor ou actividades guardadas. Uma localização recebida diferente é apresentada como divergência, mantendo a anterior para revisão explícita. Confirmar associações gera a revisão/auditoria do contexto existente; alterar apenas a referência documental não reabre etapas nem invalida cálculos se as entradas efectivas não mudarem. A alteração de localização, actividade ou outros pressupostos conserva os controlos de impacto existentes.
No simulador, o componente comum mostra as oportunidades, localizações e associações do dossier. Os campos recuperados continuam a usar os resumos editáveis da entrega anterior; respostas deliberadas do simulador não são substituídas. Relatórios e CSV do plano conservam cada oportunidade, o respectivo território e as associações escolhidas, com origem identificável, sem inferir incentivos ou autorização de execução.
Verificação do delta e regressão directamente afectada: `InvestmentPortfolioContinuityTest` e `InvestmentOpportunityChoicesTest`, seis testes/151 asserções em base QA isolada com transacções revertidas; incluem retoma após autenticação, selecção múltipla, associação partilhada entre fichas, preservação de título/localização/actividades, rejeição de origem não relacionada/privada, confirmação e recuperação no rascunho do simulador, e alteração só de referência sem invalidar cálculos. O teste de transporte dos 34 usa agora o mesmo agrupamento `selection_labels` do navegador, sem alargar os campos aceites pela API. Frontend: cinco testes territoriais/entrada e um teste de continuidade/CSV/impressão. Types e build aprovados. As provas dos incrementos anteriores mantêm-se; não se voltou a importar, publicar ou conferir os 20 artigos, 34 oportunidades, 408 limites ou 29 fichas na homologação.
Navegador QA: fixture identificada como SYNTHETIC QA, com oportunidade, âncora/associação e dossier próprios, sem atribuir a dados reais as associações do teste. Conferidos os parâmetros dos dois botões públicos, a recepção na entrada, escolha do dossier existente, rascunho guardado, prévia com Sofala/associação, confirmação e recuperação no simulador com a mesma referência. Os registos reais QA não foram reclassificados nem associados artificialmente para esta prova visual. Captura local `outputs/preservation-inventory-20261023/portfolio-dossier-simulator-continuity-QA.png`. Nenhuma escrita remota ou publicação real.
**Causa e prevenção da perda anterior:** mantém-se o diagnóstico documental de `e65cb269` e o manifesto nominal da reposição auditada. A mudança do importador para rascunhos deixou de reconciliar a divulgação anteriormente aprovada noutro ambiente; não se conhece a data exacta da perda num endereço. Nas próximas entregas, conservar a visibilidade/aprovação dos exemplares existentes e conferir os IDs/títulos/estados contra o inventário aceite, antes/depois do transporte; um transporte privado não revoga a divulgação anterior. Não restaurar a base inteira nem reimportar/publicar automaticamente conteúdos novos para corrigir visibilidade. Esta é prevenção técnica conforme a instrução de preservação da Tempo, não um novo aprovador. A presença visual não substitui o manifesto, a proveniência e o recibo de reposição existentes.
Saldo: visibilidade das 29 fichas confirmada no destino na passagem anterior; continuidade implementada neste recorte QA para associações efectivamente cadastradas. Fichas sem oportunidades associadas continuam a indicar essa lacuna; não foram preenchidas com relações presumidas. Homologação inalterada nesta passagem; aplicação transversal permanece parcial.
## Entrega QA reconciliada — preparação adaptativa, 23/10/2026
Conjunto funcional único, na ordem Git já existente: `7ca94967` → `f2150b19` → `5bda74ae`. A reconciliação documental `7d798579` conserva a evidência da carteira. Não fazer três instalações independentes, reaplicar patches ou substituir ficheiros por exemplares de cada candidata. Preservar estes commits e integrar o conjunto pelo processo Git de revisão existente.
| Candidata | Comportamento incluído | Provas aceites reutilizadas |
| --- | --- | --- |
| `7ca94967` | Ocultar preparação sem aplicação e respectivas acções; conservar estados e evidências; distinguir requisitos não aplicáveis nos relatórios | Teste API de preservação/reabertura: 18 asserções; relatório: 11 testes; types/build e cenário QA |
| `f2150b19` | Dossier → simulador: resumos editáveis dos dados recuperados, nomes territoriais ligados aos IDs correctos, edições deliberadas preservadas, percurso de análise existente | Reutilização: 12 testes; nomes exactos: 14 asserções API; types/build e gravação/recuperação visual QA |
| `5bda74ae` | Situação do terreno acompanha Não/Sim/Por definir na interface, gravação/validação, recuperação e relatórios; resposta anterior conservada | Delta de terreno: 4 testes frontend e 32 asserções API; types/build e três estados no navegador QA |
**Reconciliação Git:** base `origin/main` consultada novamente no remoto: `e1f010f941e9993645e131b34ec1f4c3fe82c71b`. As três candidatas são antecessoras do conjunto reconciliado; não há commits exclusivos de `main` a resolver nesta conferência. Os quatro worktrees existentes estavam limpos e os dois worktrees de outras entregas permaneceram intocados. Conferido o delta acumulado sem erros de whitespace; sem migrations, novas rotas ou alterações de dependências. Não foi reescrito histórico nem repetida a suite QA aceite. Esta conferência de integração não é nova prova funcional de todo o portal.
**Disponibilização e reversão:** esta entrega permanece em QA, sem integração em `main` ou deploy nesta passagem. Instalação futura usa o build e o deploy Git existentes; não requer importação, migração de dados, publicação de conteúdos ou novos papéis. Reversão de código deve conservar o payload cifrado, decisões, respostas anteriores, referências, relatórios emitidos e histórico; não restaurar uma base antiga nem eliminar campos/registos para reverter a interface. Conferir apenas o delta autorizado no destino quando a entrega for integrada.
**Formulação do saldo:** Homologação inalterada por esta entrega; aplicação transversal ainda parcial. A carteira e os projectos âncora permanecem prioritários no mesmo plano de estabilização. A visibilidade das 29 fichas foi confirmada contra o manifesto aprovado; a continuidade carteira → oportunidade → dossier → simulador permanece por concluir. Não declarar «sem reposição confirmada» para a visibilidade, pois isso contradiz a evidência registada e a conferência de leitura de `7d798579`. Não considerar encerrada a estabilização integrada por causa dessa visibilidade ou dos incrementos do simulador.
Pendência concreta preservada na matriz: transportar a origem `from_dossier` ao escolher Preparar investimento, sem presumir associações inexistentes. Nenhuma correcção dessa lacuna foi incluída silenciosamente neste conjunto.
## Carteira no percurso integrado — reconciliação do saldo, 23/10/2026
Decisão expressa da Tempo: carteira/projectos âncora e percurso adaptativo têm diagnósticos distintos, mas a separação não significa adiamento nem exclusão do percurso. A experiência deve conservar o contexto em corredor/localização → oportunidades → associações documentadas a âncoras e clusters → dossier → simulador. A recuperação da visibilidade é prioritária até confirmação da causa e da reposição; os incrementos adaptativos não constituem prova dessa reposição.
**Correcção do saldo comunicado nos últimos incrementos:** a causa e a reposição já estavam registadas na secção Preservação do concluído e no recibo existente. O importador passou a criar rascunhos sem reconciliar a aprovação documental anterior; o delta de `e65cb269` em `ImportInvestmentPortfolio.php` confirma a retirada de `--approval` e do caminho de publicação previamente aprovado. Não se atribui a essa alteração a data exacta de desaparecimento num endereço. A reposição anterior foi nominal, auditada e preservou relações/conteúdo: 15 projectos, 8 clusters, 6 corredores.
**Conferência actual de leitura na homologação:** a rota pública `/carteira` apresenta 29 fichas. Os 29 identificadores e títulos coincidem integralmente com `approved-portfolio-visibility-20261023.json`: zero ausentes e zero títulos divergentes. Contagem apresentada: 15 projectos, 8 clusters, 6 corredores. A ficha Cadeia de Valor do Arroz por Projecto de Processamento abre em `/carteira/57a4ee7a-4c07-4e26-b8f5-e07726e41512`. A sessão do navegador estava autenticada, mas foi consultada a rota pública, cujo controlador aplica a mesma consulta `visible()`; a prova anónima da reposição anterior mantém-se no recibo. Não foram executadas novas publicações, importações, escritas remotas ou reposições de dados nesta conferência.
**Continuidade ainda por concluir, distinta de desaparecimento:** a ficha de arroz consultada informa não ter oportunidades publicadas associadas. Não inferir ligações por semelhança de corredor ou cadeia de valor. No código actual, a carteira transmite `from_dossier` para uma oportunidade e a descoberta valida a origem; a ligação Preparar investimento em `investment/detail.tsx` transmite apenas `source=opportunity:{id}`. Falta preservar explicitamente essa origem documental no arranque/contexto privado. A presença das 29 fichas não demonstra essa continuidade até ao simulador, nem encerra a aplicação transversal. Manter este ponto na matriz existente e reaproveitar o percurso, sem criar cadastros paralelos ou novas associações presumidas.
## Situação do terreno acompanha a necessidade de terra — delta QA, 23/10/2026
Origem: decisão expressa da Tempo após `f2150b19`. No simulador ligado ao mesmo dossier, **Não** oculta Situação do terreno sem a exigir; **Sim** apresenta o campo com o valor já guardado e permite alterá-lo; **Por definir** identifica a pendência e permite guardar sem resposta. O simulador autónomo conserva o comportamento anterior. Não se infere a necessidade de terra da localização, do corredor ou da escolha de um documento.
A decisão vem de `investor_path.facts.land_needed`, já existente. O contexto de preparação transporta-a na referência guardada do simulador; versões anteriores recuperam a aplicabilidade S10. Informação ausente ou inválida permanece por definir. Arrendamento/DUAT já declarados no dossier podem preencher a situação correspondente quando a necessidade é Sim, sem sobrescrever uma edição deliberada no simulador. A área das instalações e os requisitos documentais/jurídicos não são alterados por inferência.
Gravação e validação: Situação do terreno continua opcional, sem criar obrigatoriedade para Sim nem para Por definir. Quando fornecida, conserva a validação das opções existentes. Na actualização do rascunho, o servidor consulta o dossier real vinculado ao seu titular, mesmo sem o parâmetro `context`, e conserva o valor e o nome anteriores enquanto o campo estiver oculto. A decisão guardada na referência é sincronizada com esse dossier; uma referência antiga não muda a decisão real. Recuperar Sim volta a mostrar o mesmo valor, editável. Valores anteriores permanecem no rascunho cifrado; fontes, decisões, resultados históricos e relatórios já emitidos não são apagados ou reescritos. Sem migration e sem alteração na homologação.
Relatórios e CSV do plano usam a mesma decisão: Sim apresenta a situação declarada ou a informação em falta; Não apresenta a explicação de não aplicabilidade à preparação actual, sem apresentar a resposta histórica como actual; Por definir apresenta a pendência. O modelo comum alimenta Word, impressão/PDF e restantes exportações do relatório. Os dados originais do rascunho permanecem conservados.
Verificação apenas do delta: quatro testes `investment-land-preparation.test.mjs` (três estados, recuperação, versão anterior, referência inválida, validação, CSV e impressão), um teste API em base QA isolada com 32 asserções (gravação incompleta, decisão real, omissão do campo, recuperação e alteração após Sim); types/build aprovados. No navegador, exclusivamente no dossier sintético QA: Não ocultou o campo; Sim permitiu guardar DUAT; Por definir mostrou a pendência e permitiu guardar; regressar a Sim recuperou DUAT seleccionado e editável. A cobertura transversal continua parcial. O saldo actual da carteira e da continuidade integrada encontra-se na reconciliação acima; este incremento não demonstra a reposição.
## Continuidade oportunidade → localização → dossier → simulador — incremento QA, 23/10/2026
Origem: decisão expressa da Tempo de aperfeiçoar o percurso adaptativo por incrementos. Mantém-se `7ca94967` como primeiro recorte QA; nenhuma destas duas alterações foi instalada na homologação nesta passagem. Diagnóstico de visibilidade e continuidade integrada da carteira: consultar a reconciliação acima; não os considerar adiados ou fora do percurso.
| Ponto encontrado | Delta neste incremento | Limite preservado |
| --- | --- | --- |
| Corredor/localização já escolhidos reapareciam no simulador como perguntas abertas | Reutilizar os mesmos identificadores e nomes do cadastro; mostrar resumos editáveis para valores herdados sem alteração local | Não inventar posto/localidade; manter as localizações específicas de todas as fichas. O simulador conserva o contexto geral da primeira escolha, sem agregar cálculos de oportunidades diferentes |
| Título, descrição e sector recuperados voltavam a pedir preenchimento | Resumo do valor recuperado e opção Alterar; falta de dado, erro de validação ou edição deliberada mantém a pergunta aberta | A gravação existente, as versões e a validação no servidor continuam a aplicar-se; não aceitar dados apenas porque o resumo foi mostrado |
| Entrar no simulador desde o dossier pedia novamente o tipo de percurso/finalidade | Entrada nova selecciona o percurso existente Avaliar o meu investimento / Apenas simular; opção Alterar disponível | Escolha técnica de navegação, não declaração jurídica. Não substituir uma finalidade previamente escolhida pelo utilizador; não escolher detalhe, perfil, regime fiscal ou respostas desconhecidas |
| Reutilização mostrava UUID, chaves e versões internas ao investidor | Componente comum apresenta nome do investimento, referências legíveis e regresso ao dossier | Identificadores, origem, versões e histórico permanecem nos registos; divergências não são resolvidas silenciosamente |
No simulador, os dados conhecidos aparecem como resumos com **Alterar**. Complete os restantes campos e use a gravação e o regresso ao dossier já existentes. Quando houver divergência, abra a comparação e seleccione expressamente os valores a substituir. Os nomes territoriais acompanham os identificadores correspondentes, incluindo após gravação/recuperação; uma escolha local divergente conserva o seu nome. Fontes sem nome recuperável não são mostradas como UUID no resumo.
Validação do delta: `context-inheritance.test.mjs` (12 testes, incluindo gravação/recuperação, edições/limpezas deliberadas, outro dossier, divergência geográfica e finalidade anterior); `InvestmentCorridorContextTest::test_simulator_reuses_names_for_the_exact_chosen_territory_and_sector_without_creating_a_location` (14 asserções, base QA isolada). A conferência visual utiliza exclusivamente o dossier sintético QA. A entrada pública/autenticação e a configuração de corredores reutilizam as provas aceites. Não se declara concluída a reutilização transversal em todos os consumidores; subsistem campos próprios do simulador e divergências a resolver pelo investidor, não dados a presumir.
Conferência visual concluída: entrada pelo mesmo dossier, percurso de análise seleccionado, título/resumo/sector como resumos editáveis, Sofala/Dondo recuperados, posto/localidade em falta sem preenchimento presumido. Uma edição deliberada do título no simulador sobreviveu à actualização da página; os restantes resumos continuaram herdados. Types e build passaram. A lacuna Situação do terreno identificada neste incremento foi tratada no delta QA descrito acima; o ocultamento de S10 em `7ca94967` não a tinha resolvido. Não alterar a aplicabilidade jurídica ou a área de instalações por inferência dessa resposta.
## Território adaptado ao corredor — decisão funcional da Tempo, 23/10/2026
Revisão funcional `0025309b` integrada pelo Git e disponível na homologação. Conferência do destino apenas de leitura: o serviço territorial para Maputo devolve exactamente as duas unidades existentes, sem consultar ou gravar um dossier real. A prova de recuperação e confirmação de Sofala/Dondo foi feita no dossier sintético QA, com indicação **Dados confirmados**. A prévia apresenta nomes e alterações úteis ao investidor, omitindo campos de identificadores internos.
@@ -270,11 +672,19 @@ Relatório desta observação: [verificação editorial](../../../outputs/invest
## Matriz executável
Decisão transversal da Tempo, 23/10/2026: percurso adaptativo com entradas por ideia/oportunidade/sector/localização; perguntar uma vez, preservar edições deliberadas e contexto entre ecrãs/autenticação; seleccionar opções únicas; ocultar exigências não aplicáveis sem confundir ausência de informação com dispensa; conservar especificidades na selecção múltipla. Mostrar posição, pendências e próxima acção. Relatórios/exportações seguem a mesma aplicabilidade. Origem: pedido expresso da Tempo nesta data; implementação incremental nos componentes existentes, sem novos aprovadores.
Incremento de apresentação: o dossier conserva as 19 etapas e o histórico, mas apresenta formulários apenas das etapas com preparação permitida. As restantes têm resumo do fundamento e podem regressar ao rever a decisão. A ferramenta Terra/DUAT deixa de ser proposta quando `land_needed=no`; `unknown` mantém a preparação disponível. Relatórios do plano reúnem requisitos documentais `not_applicable` numa explicação, mantendo requisitos em falta ou indeterminados. Esta alteração não remove obrigações jurídicas nem modifica dados guardados. A aplicação transversal a todos os formulários, entradas e exportações permanece por conferir; não se encerra esse requisito por documentação.
| Menu / ecrã | Componente React | Campos / entidade / tabela | Rota ou API | Perfil / regra | Evidência |
| ------------------------------ | ------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------- |
| Preparação adaptativa / relatório | `investor-path/show`, `investment-requirements`, `project-dossier-report` | Aplicabilidade e contexto existentes; histórico preservado | `/investimentos/{id}`, `/aplicabilidade`; exportações existentes do plano | Tempo, 23/10/2026: ocultar preparação sem aplicação; desconhecido não é dispensa | Incremento QA: `InvestmentCorridorContextTest::test_nonapplicable_land_hides_the_action_and_reopening_recovers_saved_evidence` (18 asserções); relatório 11 testes; types/build; cenário sintético no navegador. Cobertura transversal ainda parcial. |
| Dossier → simulador sem repetir dados | `dossier-prefill`, `simulation-form`, `context-inheritance` | Campos existentes, `context_reuse`, `selection_labels`; sem migration | `/investimentos/{id}/contexto`, `/simulador-fiscal?context={id}` | Resumos apenas para valores herdados intactos; alteração deliberada preservada; perfil/detalhe/regime não presumidos | Incremento QA de continuidade, 23/10/2026: 12 testes de reutilização e 14 asserções de nomes exactos; cobertura geral permanece parcial |
| Situação do terreno adaptada | `simulation-form`, `investment-land-preparation`, `project-dossier`; `SimulationFormDrafts` | `land_needed` existente; situação e rótulo conservados no rascunho cifrado | APIs existentes de contexto/rascunho e exportações do plano | Tempo: Não oculta, Sim recupera e permite editar, Por definir identifica pendência sem bloquear rascunho | Delta QA 23/10/2026: 4 testes frontend e 32 asserções API; sem migration; homologação não alterada |
| Início | `PortalInvestidor` | Sectores, oportunidades, recursos publicados | `/`, APIs existentes de homepage | Visitante; contagens do cadastro | `PublicPortalNavigationTest`, navegador |
| Catálogo / Educação | `investment/catalogue` | Filtros, educação, pivots, facetas | `/oportunidades`, `/educacao`; `/api/v1/investment/search` | Visitante; filtros em intersecção, montantes com moeda | `InvestmentUnificationTest` |
| Cluster / Corredor / Âncora | `portfolio/index`, `portfolio/show` | Dossiers, relações, cadeias e oportunidades | `/clusters`, `/corredores`, `/projectos-ancora`, `/carteira/{id}` | Visitante; dossier publicado; cadeia pública | `InvestmentPortfolioTest`, testes territoriais |
| Continuidade carteira → oportunidade → dossier → simulador | Componentes de carteira, `investment/detail`, contexto existente | Origem documental por oportunidade no payload cifrado; associações existentes, sem inferência | `from_dossier` e território no arranque; rascunho, prévia, confirmação e simulador existentes | Mesmo percurso integrado; origem relacionada/publicada; respostas deliberadas conservadas | Visibilidade: 29 IDs/títulos conferidos anteriormente. Delta QA: seis testes/151 asserções API, cinco testes de entrada territorial, um de relatório; instalação na homologação pendente |
| Ficha | `investment/detail` | Oportunidade, contexto, relações, fonte | `/oportunidades/{slug}`; `/api/v1/investment/opportunities/{slug}` | Visitante; rascunhos não acessíveis | `PublicPortalNavigationTest`, `InvestmentUnificationTest` |
| Mapa | Componente `Map` em catálogo | `geo_locations`, `geo_geometries`, `opportunity_locations` | `/api/v1/investment/map` | Visitante; dois níveis de classificação pública | Teste PostGIS e estados vazios |
| Comparar / exportar / sugerir | `investment/catalogue` | IDs publicados, dados de catálogo | `/compare`, `/export`, `/suggest` sob prefixo da API | Visitante; 4 comparações, 1.000 linhas/pontos; CSV protegido | Teste de publicação e fórmula CSV |
@@ -2637,4 +3047,4 @@ Preservação por comparação dos registos preexistentes: **32 documentos, 31 r
**Guia da equipa no destino:** abrir a [gestão única](https://teste-investidor.tempo.co.mz/gestao-conteudos) → **Gerir conteúdos, versões e histórico** → escolher **Rascunhos**, pesquisar o título e abrir **Editar artigo** ou **Ler / pré-visualizar artigo**. Corrigir o texto e o tratamento de imagem → **Pré-visualizar alterações** → **Guardar artigo como rascunho** → **Ler revisão guardada**. Usar **Voltar ao acervo para metadados e publicação** para corrigir título, descrição, origem, enquadramento, acesso e direitos. **Publicar** continua separado, conforme a autorização e os campos existentes, quando a equipa decidir publicar. Guardar uma edição de um artigo publicado retira-o até nova publicação expressa. Não há comandos ou intervenção do Codex neste percurso corrente.
Recibo: [cadastro, acessos e preservação no destino](legacy-editorial-installation-receipt-20261022.json). A [lista única dos 50](../v2/auditoria-portal/legado-correspondencia-integral.md) e o CSV existente contêm os endereços **privados** efectivos dos 28; não são links de artigos já publicados. Capturas locais em `outputs/legacy-editorial-install-20261022/`, incluindo `editor-recovered-homologation.png`. Mantêm-se dois originais incompletos e a revisão editorial dos textos pela equipa. A Tempo não declarou ter conferido commit, pacote ou guia. **A carteira vazia e os projectos âncora continuam como falha separada, sem causa confirmada; esta entrega não a resolve.**
Recibo: [cadastro, acessos e preservação no destino](legacy-editorial-installation-receipt-20261022.json). A [lista única dos 50](../v2/auditoria-portal/legado-correspondencia-integral.md) e o CSV existente contêm os endereços **privados** efectivos dos 28; não são links de artigos já publicados. Capturas locais em `outputs/legacy-editorial-install-20261022/`, incluindo `editor-recovered-homologation.png`. Mantêm-se dois originais incompletos e a revisão editorial dos textos pela equipa. A Tempo não declarou ter conferido commit, pacote ou guia. **A carteira vazia e os projectos âncora continuam como falha separada, sem causa confirmada; esta entrega não a resolve.** (Saldo histórico desta entrega de 22/10; superado pela reposição auditada e reconciliação de 23/10 descritas acima. Não representa o estado actual.)
@@ -0,0 +1,174 @@
{
"status": "installed_in_homologation_delta_verified",
"date": "2026-10-23",
"target": "https://teste-investidor.tempo.co.mz",
"served_commit": "8036b9b071229dbf7e85ea748db70abcbeefc379",
"functional_commits": [
"1afdfee8",
"2856240c"
],
"previous_main": "55136b446deb710adf58ce2460e571bb19e94375",
"public_revision": {
"commit": "8036b9b071229dbf7e85ea748db70abcbeefc379",
"branch": "main",
"built_at": "2026-10-23T16:46:11.203132+00:00",
"manifest_sha256": "6021421c4ab12146c5fae9c8966e3dae5c8bd1361132e12184e65e00b7e8951e"
},
"installer": {
"status": "installed",
"commit": "8036b9b071229dbf7e85ea748db70abcbeefc379",
"release": "/var/www/investmoz-homologacao/releases/8036b9b071229dbf7e85ea748db70abcbeefc379-5e851af2",
"previous_release": "/var/www/investmoz-homologacao/releases/55136b446deb710adf58ce2460e571bb19e94375-cad6589e",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"8036b9b071229dbf7e85ea748db70abcbeefc379"
],
"finished_at": "2026-10-23T16:46:13.798467+00:00"
},
"pipeline_summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.30s",
"OK (57 tests, 309 assertions)",
"# tests 214",
"# pass 214",
"# fail 0"
],
"preservation": {
"fast_forward": true,
"force": false,
"protected_files": {
"app/Services/InvestmentComplements.php": true,
"resources/js/components/investment-complements.tsx": true,
"docs/investmoz/unification/continuity-corrections-installation-receipt-20261023.json": true
},
"matrix_changed_rows": [
"AUD-REL-02",
"AUD-LEG-06",
"AUD-SIM-INT03",
"AUD-SIM-INT06"
],
"unrelated_matrix_records_preserved": 3419,
"schema_config_fixtures_shipped": false
},
"native_saved_pdf": {
"actual_file": "C:\\laragon\\www\\portal-investidor\\outputs\\multi-opportunity-fix-20261023\\Relatório — SYNTHETIC QA - escolha comum aos tres consumidores.pdf",
"sha256": "010eb4ea12015563c1b2f8bc255b1078c276f824c50690732541d5bffe34b8a6",
"pages": 10,
"revision": 13,
"checks": {
"second_opportunity": true,
"manica": true,
"land_requirement": true,
"sector_requirement": true,
"human_state": true,
"technical_state_absent": true,
"first_location_absent": true,
"total": true,
"secured": true,
"gap": true
},
"visual_review": {
"status": "passed",
"pages_inspected": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
],
"pagination": "1/10 through 10/10; A4; repeated table headers; no clipped rows or visible overlaps",
"last_page": "sparse final page contains final source rows and annex statement; not blank"
},
"discovery": {
"machine": "ONALA",
"user": "macbook",
"session": "Console",
"extension": ".pdf",
"size_bytes": 191637,
"requested_basename_not_found": "candidata-1afdfee8.pdf",
"actual_basename": "Relatório — SYNTHETIC QA - escolha comum aos tres consumidores.pdf",
"resolution": "existing file discovered by listing the exact requested directory; no rename, copy or PDF regeneration"
},
"regenerated": false
},
"installed_delta": {
"real_opportunity": 53,
"scope": {
"case_id": "958df138-7980-4685-8389-fa90be02ed6b",
"case_version": 5,
"context_revision": 1,
"simulator_opportunity": "opportunity:53",
"snapshot_choices": [
"opportunity:53"
],
"snapshot_territory": "Sofala",
"land_need": "no",
"land_step": {
"code": "S10",
"title": "Regularização da terra",
"state": "dispensed",
"applicability": "not_needed_for_preparation",
"reason": "",
"context_review_required": true
},
"requirements_scopes": [
"opportunity:53"
]
},
"checks": [
"new fiche links preserve opportunity53",
"dossier explicitly selects same opportunity and keeps tool query scope",
"authenticated context GET returns canonical simulator opportunity53 and its own requirements",
"existing no-land decision hides land action in the inspected dossier",
"15 changed functional file hashes equal Git release sources"
],
"case_version_unchanged_from_prior_receipt": true,
"requests_created_updated_submitted": 0,
"accepted_business_tests_repeated": false,
"qa_second_opportunity_cases_reused": true
},
"remaining_scope": {
"anchors": "analysis6985f82e: two implementation gaps, territorial reconciliation and directed interest/authentication proof; not implemented by this delivery",
"statistics_and_AI": "deferred"
},
"evidence": [
{
"path": "outputs/treatment-install-20261023/integration-preservation.json",
"sha256": "c6fcae5f64986d13216948589a35d1fa2d9f62229f4bd7a8a8c9f559f463fd6b"
},
{
"path": "outputs/treatment-install-20261023/installer-status.json",
"sha256": "f7875ffe9fa47092e5f0288ecbd97f4f016413628b2faed6815977287c26461e"
},
{
"path": "outputs/treatment-install-20261023/public-revision.json",
"sha256": "23325ab4cce9fced625b237c9793e3af309d923f12abbd047de96670611c8cac"
},
{
"path": "outputs/treatment-install-20261023/installed-hashes.json",
"sha256": "91f9df68c0d3b9c0ace6948145362f303291c5698bb2594bff9bb9c3aabc159c"
},
{
"path": "outputs/treatment-install-20261023/installed-links.json",
"sha256": "bf73f59ecfaccf8d10f45a72c36bd684e1b12abb667a65e0d2cda4dd396eb324"
},
{
"path": "outputs/treatment-install-20261023/installed-scope-summary.json",
"sha256": "fd410fa10ee63b8c09c311530cab78ca4f1f2b961c3d31cc78c40b86bc7af255"
},
{
"path": "outputs/treatment-install-20261023/installed-dossier.txt",
"sha256": "b5577b82ad0bce03131ffddbc365f1d039229549d6b83227ad57baffd1037f07"
},
{
"path": "outputs/treatment-install-20261023/installed-dossier.png",
"sha256": "9a3133a9fb6ca11fc64e0681393d518676bb72bceb75bfc95950466271158509"
}
]
}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+11 -209
View File
@@ -9,220 +9,22 @@
@import './tempo-header.css';
/* Institutional shell adapted from Tempo; page content keeps its own theme. */
.tempo-header {
position: sticky;
top: 0;
z-index: 40;
background: #090909;
color: #fff;
}
.tempo-header-inner {
grid-template-rows: 84px;
}
.tempo-logo-link {
display: inline-flex;
align-items: center;
width: 140px;
border-radius: 0.25rem;
}
.tempo-nav-trigger {
display: flex;
min-height: 48px;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.9375rem;
font-weight: 600;
color: #eee;
transition:
color 0.2s,
background 0.2s;
}
.tempo-nav-trigger:hover,
.tempo-nav-trigger[data-state='open'] {
background: #ffffff0d;
color: #fff;
}
.tempo-nav-trigger[data-current-group] {
color: #fff;
box-shadow: inset 0 -2px #ed1c24;
}
.tempo-nav-trigger[data-state='open'] > svg {
transform: rotate(180deg);
}
@media (max-width: 640px) {
.tempo-nav-trigger {
gap: 0.25rem;
padding-inline: 0.25rem;
font-size: 0.8125rem;
}
.tempo-nav-trigger > svg {
width: 12px;
height: 12px;
@keyframes portal-hero-caret {
50% {
opacity: 0;
}
}
.tempo-utility-button {
display: flex;
min-height: 40px;
align-items: center;
gap: 0.4rem;
padding: 0.4rem;
border-radius: 0.5rem;
font-size: 0.75rem;
color: #e2e8f0;
.portal-hero-caret {
animation: portal-hero-caret 0.8s step-end infinite;
}
.tempo-utility-button:hover {
background: #ffffff10;
}
.tempo-header-social {
display: flex;
width: 30px;
height: 40px;
align-items: center;
justify-content: center;
color: var(--social-color);
transition:
transform 0.2s,
opacity 0.2s;
border-radius: 0.4rem;
}
.tempo-header-social:hover {
transform: translateY(-2px);
opacity: 0.8;
}
@media (max-width: 360px) {
.tempo-header-social {
width: 26px;
@media (prefers-reduced-motion: reduce) {
.portal-hero-caret {
animation: none;
}
}
.tempo-login-button {
display: inline-flex;
min-height: 40px;
align-items: center;
gap: 0.5rem;
border: 1px solid #ffffff50;
border-radius: 0.4rem;
padding: 0.5rem 0.85rem;
font-size: 0.8rem;
font-weight: 600;
color: #fff;
transition:
background 0.2s,
border-color 0.2s;
}
.tempo-login-button:hover {
background: #ed1c24;
border-color: #ed1c24;
}
.tempo-header-actions {
order: 2;
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.tempo-interest-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0.5rem 0.75rem;
border: 1px solid #ed1c24;
border-radius: 0.4rem;
background: #ed1c24;
color: white;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
transition: background 0.2s;
}
.tempo-interest-button:hover {
background: #c9151c;
}
.tempo-panel-summary {
order: 4;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
min-height: 44px;
padding-block: 0.5rem;
border-top: 1px solid #ffffff1a;
font-size: 0.75rem;
}
.tempo-panel-summary a {
color: #fff;
text-underline-offset: 3px;
}
.tempo-panel-summary a:hover {
text-decoration: underline;
}
.tempo-panel-summary span {
color: #cbd5e1;
font-size: 0.6875rem;
white-space: nowrap;
}
@media (max-width: 480px) {
.tempo-header-actions {
flex-direction: column;
align-items: flex-end;
padding-block: 0.65rem;
}
}
@media (min-width: 640px) {
.tempo-header-inner {
grid-template-rows: 90px;
}
.tempo-logo-link {
width: 160px;
}
}
@media (min-width: 1280px) {
.tempo-header-inner {
grid-template-rows: 96px;
}
.tempo-logo-link {
width: 152px;
}
.tempo-header-actions {
order: 6;
margin-left: 0;
}
.tempo-header-inner .tempo-nav-trigger {
padding-inline: 0.5rem;
font-size: 0.9375rem;
}
.tempo-panel-summary {
order: 0;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 0.1rem;
width: auto;
min-width: 102px;
border-top: 0;
border-left: 1px solid #ffffff1a;
padding-left: 0.75rem;
}
}
@media (min-width: 1440px) {
.tempo-header-inner {
grid-template-rows: 102px;
}
.tempo-logo-link {
width: 168px;
}
.tempo-header-inner .tempo-nav-trigger {
font-size: 1rem;
}
}
.tempo-header :is(a, button):focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.tempo-dropdown {
z-index: 60;
border: 1px solid #e2e8f0;
@@ -153,7 +153,7 @@ export function NavigationContextProvider({
}
const view = focus
? await contextApi<NavigationView>(
`/investimentos/${focus.case_id}`,
`/investimentos/${focus.case_id}${direct===focus.case_id && new URL(url,'https://context.invalid').searchParams.get('opportunity') ? '?opportunity='+encodeURIComponent(new URL(url,'https://context.invalid').searchParams.get('opportunity')!) : ''}`,
)
: null;
if (!stopped) {
@@ -234,13 +234,16 @@ export function NavigationContextSummary() {
return null;
const saved = view?.investment_context;
const ref = saved?.selection;
const params = new URLSearchParams(url.split('?')[1] ?? '');
const scopedChoice = saved?.opportunity_choices?.find(c=>c.opportunity.key===params.get('opportunity'));
const multiple = (saved?.opportunity_choices?.length ?? 0)>1;
const currentStep = view?.steps.find((s) => s.code === step);
const sector =
const sector = scopedChoice ? scopedChoice.opportunity.fields.sector || 'Não indicado' :
ref?.sector?.title ||
saved?.fields.sector ||
view?.sector_contexts.map((s) => s.name).join(' · ') ||
'Não indicado';
const location =
const location = scopedChoice ? scopedChoice.territory?.title || 'Por definir' : multiple ? 'Consulte a localização de cada oportunidade' :
saved?.fields.location ||
ref?.territory?.title ||
ref?.park?.title ||
@@ -260,7 +263,7 @@ export function NavigationContextSummary() {
view?.case.title ||
'Selecione um dossier',
Oportunidade:
ref?.opportunity?.title || 'Sem oportunidade associada',
scopedChoice?.opportunity.title || (multiple ? 'Várias — oportunidade em tratamento no formulário' : ref?.opportunity?.title || 'Sem oportunidade associada'),
Sector: sector,
Localização: location,
'Etapa atual': currentStep
@@ -282,7 +285,7 @@ export function NavigationContextSummary() {
{view && (
<Link
className="font-semibold underline"
href={`/investimentos/${active}`}
href={`/investimentos/${active}${scopedChoice ? '?opportunity='+encodeURIComponent(scopedChoice.opportunity.key) : ''}`}
>
Abrir dossier atual
</Link>
@@ -290,7 +293,7 @@ export function NavigationContextSummary() {
{view?.next && (
<Link
className="underline"
href={`/investimentos/${active}#${view.next.code}`}
href={`/investimentos/${active}${scopedChoice ? '?opportunity='+encodeURIComponent(scopedChoice.opportunity.key) : ''}#${view.next.code}`}
>
Preparar {view.next.code}
</Link>

Some files were not shown because too many files have changed in this diff Show More