fix: keep opportunity preparation focused on the pending selection
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-23 12:29:26 +02:00
parent f1ac82ae4d
commit 7226287526
5 changed files with 47 additions and 55 deletions
@@ -1,5 +1,13 @@
# Rastreabilidade, utilização e formação
## Continuidade entre escolha e preparação — 23/10/2026
Falha reportada pela Tempo: depois de criar o dossier para Florestas/Beira, o editor começava por uma avaliação do estado ainda não confirmado (`contexto v0`). A escolha solicitada vinha separadamente em `source`/`requestedChoices`; não era outro investimento, mas a avaliação do estado guardado vazio. A ligação do quadro voltava ao editor sem os parâmetros da escolha pendente, permitindo perder essa referência antes de confirmar. Não foi identificada nem corrigida por escrita uma troca de dados entre dossiers.
O editor passa a começar por **Preparar investimento**, pela oportunidade e pela localização por ficha. Retirado o quadro de avaliação do estado guardado deste ecrã de edição, incluindo a ligação que descartava a escolha pendente. Requisitos guardados só são apresentados quando correspondem às escolhas que estão no formulário. Botões coerentes: **Criar dossier e preparar investimento**, **Rever dados antes de guardar**, **Guardar dados do investimento**. A confirmação continua necessária para guardar alterações; abrir uma referência não altera o dossier.
Nos consumidores de avaliação, retirados números internos de contexto/versão, explicações de implementação, quadro genérico de ausência de zona e elementos sem título na lista de etapas. Mantidas condições úteis, fontes oficiais e limites efectivos do simulador. O exemplo real foi recuperado por GET com `source=opportunity:53` no dossier indicado pela Tempo, sem guardar ou criar dados. Teste QA isolado: 20 asserções sobre recuperação da escolha e igualdade do contexto antes/depois da abertura. Tipos e build conferidos; sem migrations ou alterações de cálculo, autorização ou dados activos.
## Dossier orientado ao investidor — 23/10/2026
Correcção da apresentação solicitada pela Tempo: o componente partilhado de documentos do contexto mostrava, por defeito, identidade técnica, versões, hashes, notas de permissões e mensagens de ausência de suporte. Era reutilizado na entrada, edição e consulta do dossier; a entrada repetia ainda a mesma oportunidade num bloco de origem técnica.
@@ -61,6 +61,8 @@ export function InvestmentContextAssessment({
if (!snapshot) return null;
const fields = snapshot.fields ?? {};
const zone = snapshot.zone;
const steps = (snapshot.path?.steps ?? []).filter(step => !!step.title?.trim());
if (!zone && !fields.investor_origin && !fields.tax_residency_country && !snapshot.preparation?.enabled) return null;
const regime = zone?.fields?.investment_regime ?? fields.investment_regime;
const names: Record<string, string> = {
zee: 'ZEE',
@@ -73,10 +75,9 @@ export function InvestmentContextAssessment({
aria-label="Aplicabilidade ao investimento"
>
<h3 className="font-semibold">
Aplicabilidade ao investimento · contexto v
{snapshot.context_revision}
Condições do investimento
</h3>
<p>
{(fields.investor_origin || fields.tax_residency_country) && <p>
Origem do investidor:{' '}
{{
national: 'nacional',
@@ -84,9 +85,8 @@ export function InvestmentContextAssessment({
mixed: 'misto',
}[fields.investor_origin] ?? 'informação em falta'}
. Residência fiscal declarada:{' '}
{fields.tax_residency_country || 'informação em falta'}. Estas
condições são distintas; uma não determina a outra.
</p>
{fields.tax_residency_country || 'Por preencher'}.
</p>}
{zone ? (
<>
<p>
@@ -95,8 +95,7 @@ export function InvestmentContextAssessment({
{names[regime ?? ''] ?? 'zona seleccionada'}.
</strong>{' '}
<Link className="underline" href={zone.href}>
{zone.title} · versão {zone.version} · consultar
ficha e fundamentos
{zone.title} · consultar condições
</Link>
.
</p>
@@ -162,39 +161,19 @@ export function InvestmentContextAssessment({
</p>
)}
<p role="status">
Benefício fiscal deste enquadramento: indeterminado. Não
existe neste consumidor uma regra executável ligada ao
reconhecimento e condições da zona. No simulador, o IVA
disponível corresponde apenas ao cenário geral sem
benefício, sobre vendas declaradas compatíveis; não
simula o regime especial. O cenário com benefício e o
apuramento final estão indisponíveis.
Os benefícios desta zona ainda não estão disponíveis no simulador. O cenário geral disponível não inclui benefícios especiais nem apuramento fiscal final.
</p>
</>
) : (
<p>
Enquadramento geral de preparação: sem ZEE, ZFI ou parque
seleccionado. Isso não confirma a elegibilidade para uma
operação fiscal. O cálculo mantém o âmbito e as exclusões da
versão publicada.
</p>
)}
<p>
Confirmado neste quadro significa apenas dado recuperado do
dossier; declarações e referências não são decisões jurídicas.
Requisitos específicos não documentados permanecem
indeterminados.
</p>
{snapshot.path && (
) : null}
{!!steps.length && (
<details>
<summary>
Passos adaptados pelo percurso existente · versão{' '}
{snapshot.path.definition_version}
Etapas a preparar
</summary>
<ul className="list-disc pl-5">
{snapshot.path.steps.map((step) => (
{steps.map((step) => (
<li key={step.code}>
{step.code} — {step.title}:{' '}
{step.title}:{' '}
{step.context_review_required
? 'informação alterada; rever fundamento'
: step.applicability ===
@@ -224,7 +203,7 @@ export function InvestmentContextAssessment({
className="underline"
href={`/investimentos/${contextId}/contexto/editar`}
>
Alterar contexto e comparar impacto antes de guardar
Rever dados do investimento
</Link>
)}
</section>
@@ -69,7 +69,7 @@ export default function ContextStart({
>
{step && !source
? 'Abrir ' + step + ' no mesmo dossier'
: 'Comparar no mesmo dossier'}
: 'Preparar neste dossier'}
</Link>
)}
<p>
@@ -120,7 +120,7 @@ export default function ContextStart({
Autorizo reutilizar o meu perfil neste dossier privado.
</label>
<button className={button} disabled={!consent || busy}>
Criar dossier e comparar a referência
Criar dossier e preparar investimento
</button>
{error && <p role="alert">{error}</p>}
</form>
+9 -18
View File
@@ -2,7 +2,6 @@ import {
PreparationEditor,
PreparationImpact,
} from '@/components/private-investment-preparation';
import { InvestmentContextAssessment } from '@/components/investment-context-assessment';
import ContextDocuments from '@/components/context-documents';
import { Link } from '@inertiajs/react';
import { useState } from 'react';
@@ -167,7 +166,7 @@ export default function ContextEditor({
setPlan(null);
setOperation(createOperationKey());
setMessage(
'Contexto guardado no mesmo dossier. Resultados afetados identificados para revisão; conteúdo submetido e histórico preservados.',
'Dados guardados. Pode continuar a preparação do investimento.',
);
} catch (e) {
setError(
@@ -180,28 +179,18 @@ export default function ContextEditor({
}
}
return (
<Frame title="Contexto do investimento">
<InvestmentContextAssessment
contextId={view.case.id}
raw={view.prefill.simulator?.context_applicability_snapshot}
/>
<ContextDocuments source={view.context.selection.opportunity} />
<InvestmentRequirements assessment={view.context.requirements_assessment} contextId={view.case.id}/>
<Frame title="Preparar investimento">
<div className="space-y-5 [overflow-wrap:anywhere]">
<Link
className="underline"
href={`/investimentos/${view.case.id}`}
>
Voltar ao mesmo dossier
Voltar ao dossier
</Link>
<p>
{view.case.title}
</p>
<p className="rounded border border-emerald-300 bg-emerald-50 p-3">
As referências apoiam a preparação. Associar uma
oportunidade, sector ou localização não confirma incentivos,
licença, terra disponível, reserva ou autorização.
</p>
<p>Confirme as oportunidades e a localização, complete os dados disponíveis e reveja antes de guardar.</p>
{error && (
<div
role="alert"
@@ -220,9 +209,11 @@ export default function ContextEditor({
{message && <p role="status">{message}</p>}
<section className={card + ' space-y-4'}>
<OpportunityChoices choices={choices} disabled={busy} onChange={next=>{setChoices(next);setManageChoices(true);setPlan(null);}}/>
<ContextDocuments source={source ?? view.context.selection.opportunity} />
{view.context.revision > 0 && !Object.keys(changes).length && JSON.stringify(choices) === JSON.stringify(view.context.opportunity_choices ?? []) && <InvestmentRequirements assessment={view.context.requirements_assessment} contextId={view.case.id}/>}
<label className="block">Acrescentar uma oportunidade à escolha<select className={input} value="" onChange={e=>{const o=options.opportunities.find(o=>o.key===e.target.value);if(o && !choices.some(c=>c.opportunity.key===o.key)){setChoices([...choices,{opportunity:{...o,id:o.key.split(':')[1],href:'/oportunidades',version:null,sha256:'',fields:{}},territory:null,location_reason:'',location_state:'undetermined'}]);setManageChoices(true);setPlan(null);}}}><option value="">Seleccionar ficha</option>{options.opportunities.filter(o=>o.kind==='opportunity'&&!choices.some(c=>c.opportunity.key===o.key)).map(o=><option key={o.key} value={o.key}>{o.title}</option>)}</select></label>
<h2 className="text-xl font-bold">
Escolher referências existentes
Dados do investimento
</h2>
<div className="flex flex-wrap gap-2">
<label className="min-w-0 flex-1">
@@ -574,7 +565,7 @@ export default function ContextEditor({
disabled={busy}
onClick={() => void preview()}
>
Comparar impacto antes de guardar
Rever dados antes de guardar
</button>
</section>
{plan && (
@@ -706,7 +697,7 @@ export default function ContextEditor({
disabled={busy}
onClick={() => void confirm()}
>
Confirmar e guardar o contexto
Guardar dados do investimento
</button>
</div>
</section>
@@ -33,6 +33,20 @@ final class InvestmentOpportunityChoicesTest extends TestCase
return $this->postJson('/investimentos', ['title' => 'QA: investimento conjunto', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
}
public function test_edit_recovers_requested_opportunity_without_presenting_it_as_already_saved(): void
{
$record = Opportunity::where('management_creation_key', app(CorridorAgribusinessOpportunities::class)->entries()[0]['key'])->firstOrFail();
$id = $this->dossier();
$before = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json('context');
$this->get('/investimentos/'.$id.'/contexto/editar?source=opportunity:'.$record->id)->assertOk()
->assertInertia(fn (\Inertia\Testing\AssertableInertia $page) => $page->component('investor-path/context')
->where('source.key', 'opportunity:'.$record->id)
->where('requestedChoices.0.opportunity.key', 'opportunity:'.$record->id)
->where('requestedChoices.0.opportunity.title', $record->title));
$after = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json('context');
$this->assertSame($before, $after);
}
public function test_multiple_opportunities_keep_sites_sources_requirements_and_results_in_one_dossier(): void
{
$entries = array_slice(app(CorridorAgribusinessOpportunities::class)->entries(), 0, 2);