Adapt land status to saved investment land need
This commit is contained in:
@@ -563,6 +563,7 @@ final class InvestorContext
|
||||
$case = $this->desk->case($actor, $id);
|
||||
abort_unless($this->desk->owns($actor, $case, false) || $actor->can('manage-portal'), 403);
|
||||
$ctx = $this->state($case);
|
||||
$ctx['land_preparation'] = $this->landPreparation($case);
|
||||
$path = app(InvestorPath::class)->view($actor, $id);
|
||||
$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']))))];
|
||||
@@ -591,6 +592,14 @@ 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 landPreparation(\stdClass $case): array
|
||||
{
|
||||
$path = $this->desk->decode($case->payload)['investor_path'] ?? [];
|
||||
$need = $path['facts']['land_needed'] ?? 'unknown';
|
||||
|
||||
return ['need' => in_array($need, ['yes', 'no'], true) ? $need : 'unknown', 'reason' => $path['fact_reasons']['land_needed'] ?? ''];
|
||||
}
|
||||
|
||||
public function prefill(array $ctx): array
|
||||
{
|
||||
$f = $ctx['fields'];
|
||||
@@ -598,6 +607,10 @@ final class InvestorContext
|
||||
// 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) {
|
||||
@@ -613,6 +626,7 @@ final class InvestorContext
|
||||
'opportunity_choices' => $ctx['opportunity_choices'] ?? [],
|
||||
'requirements_assessment' => $ctx['requirements_assessment'] ?? null,
|
||||
'path' => $ctx['path_applicability'] ?? null,
|
||||
'land_preparation' => $ctx['land_preparation'] ?? null,
|
||||
'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.',
|
||||
|
||||
@@ -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,34 @@ 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
|
||||
{
|
||||
$decision = app(InvestorContext::class)->landPreparation($case);
|
||||
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;
|
||||
$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');
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Rastreabilidade, utilização e formação
|
||||
|
||||
## 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. Carteira e projectos âncora mantêm-se como incidente separado.
|
||||
|
||||
## 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. Carteira e projectos âncora continuam como incidente separado.
|
||||
@@ -15,7 +27,7 @@ No simulador, os dados conhecidos aparecem como resumos com **Alterar**. Complet
|
||||
|
||||
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. Lacuna adicional identificada para o próximo recorte: a pergunta **Situação do terreno** do simulador ainda não usa a decisão de necessidade de terra do dossier; não se considera resolvida pelo ocultamento de S10 em `7ca94967`. Não alterar a aplicabilidade jurídica ou a área de instalações por inferência dessa resposta.
|
||||
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
|
||||
|
||||
@@ -295,6 +307,7 @@ Incremento de apresentação: o dossier conserva as 19 etapas e o histórico, ma
|
||||
| ------------------------------ | ------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------- |
|
||||
| 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 |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
|
||||
import { SimulationMonthlyIva } from './simulation-monthly-iva';
|
||||
import { inheritSimulatorFields, reusedInvestmentValue } from '@/lib/context-inheritance';
|
||||
import { investmentLandPreparation } from '@/lib/investment-land-preparation';
|
||||
import {
|
||||
PublicDocumentContent,
|
||||
type PublicDocument,
|
||||
@@ -3283,6 +3284,16 @@ export function SimulationForm() {
|
||||
: 'Avalie um projecto definido. A análise fiscal acompanha os fluxos financeiros, mas não prova rentabilidade nem concede incentivos.'}
|
||||
</output>
|
||||
)}
|
||||
{current.id === 'location' && contextId && (
|
||||
<aside className="rounded border p-3" aria-label="Necessidade de terra">
|
||||
<p>{investmentLandPreparation(draft.context_applicability_snapshot).need === 'no'
|
||||
? 'Não precisa de terra neste investimento. A situação do terreno não é pedida; os valores anteriores ficam conservados.'
|
||||
: investmentLandPreparation(draft.context_applicability_snapshot).need === 'yes'
|
||||
? 'Precisa de terra neste investimento. Reveja a situação do terreno já preenchida ou indique-a quando disponível.'
|
||||
: 'Falta definir se precisa de terra. Pode guardar o rascunho e continuar sem preencher a situação do terreno.'}</p>
|
||||
<a className="underline" href={`/investimentos/${contextId}`}>Rever a necessidade de terra no dossier</a>
|
||||
</aside>
|
||||
)}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{fields.map((field) => {
|
||||
const fieldRequired =
|
||||
|
||||
@@ -19,6 +19,12 @@ export function inheritCompatibleFields(scope:string, values:Record<string,unkno
|
||||
const geoConflict=geo.some(key=>(!empty(values[key]) || key in previous) && signature(values[key])!==signature(source[key]) && previous[key]!==signature(values[key]));
|
||||
for(const [key,value] of Object.entries(source)) {
|
||||
if ((allowed && !allowed.includes(key)) || (empty(value) && !(key in previous))) continue;
|
||||
if (key === 'context_applicability_snapshot') {
|
||||
// Read-only assessment of the current dossier, not a deliberate form answer.
|
||||
if (signature(values[key]) !== signature(value)) patch[key] = value;
|
||||
next[key] = signature(value); inherited.push(key);
|
||||
continue;
|
||||
}
|
||||
if (key.endsWith('_label')) {
|
||||
const base = key.slice(0, -6);
|
||||
const baseOwned = previous[base] === signature(values[base]);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export type LandPreparation = { need: 'yes' | 'no' | 'unknown'; reason: string };
|
||||
|
||||
/** Preparation decision from the same saved dossier, never evidence of a land right. */
|
||||
export function investmentLandPreparation(raw: unknown): LandPreparation {
|
||||
try {
|
||||
const snapshot = JSON.parse(String(raw ?? ''));
|
||||
if (snapshot.version !== 1) return { need: 'unknown', reason: '' };
|
||||
const decision = snapshot.land_preparation;
|
||||
if (decision && ['yes', 'no', 'unknown'].includes(decision.need))
|
||||
return { need: decision.need, reason: typeof decision.reason === 'string' ? decision.reason : '' };
|
||||
// Existing snapshots already carry S10 applicability; no reimport is required.
|
||||
const step = Array.isArray(snapshot.path?.steps) ? snapshot.path.steps.find((row: { code?: string } | null) => row?.code === 'S10') : null;
|
||||
return { need: step?.applicability === 'not_needed_for_preparation' ? 'no' : step?.applicability === 'applicable' ? 'yes' : 'unknown', reason: typeof step?.reason === 'string' ? step.reason : '' };
|
||||
} catch { return { need: 'unknown', reason: '' }; }
|
||||
}
|
||||
|
||||
export function landStatusVisible(draft: Record<string, unknown>): boolean {
|
||||
return !draft.context_applicability_snapshot || investmentLandPreparation(draft.context_applicability_snapshot).need === 'yes';
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { displayValue } from './display-value.ts';
|
||||
import { investmentLandPreparation } from './investment-land-preparation.ts';
|
||||
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
@@ -686,6 +687,12 @@ export function buildProjectDossierRows(
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (draft.context_applicability_snapshot) {
|
||||
const land = investmentLandPreparation(draft.context_applicability_snapshot);
|
||||
rows.push(['Localização', 'Necessidade de terra', land.need === 'no' ? 'Não' : land.need === 'yes' ? 'Sim' : 'Por definir', land.reason || 'Decisão de preparação do dossier; não comprova direitos sobre a terra']);
|
||||
if (land.need === 'yes') rows.push(['Localização', 'Situação do terreno', String(draft.land_status_label ?? ({ owned: 'Próprio', duat: 'DUAT', leased: 'Arrendado', requested: 'Pedido em curso', unknown: 'Por determinar' } as Record<string, string>)[String(draft.land_status)] ?? 'Informação em falta'), 'Informação declarada; não comprova título ou disponibilidade']);
|
||||
else rows.push(['Localização', 'Preparação do terreno', land.need === 'no' ? 'Situação do terreno não aplicável à preparação actual; valores anteriores conservados.' : 'Definir se precisa de terra; pode continuar a preparar o investimento.', 'Sem inferência de dispensa legal']);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { inheritSimulatorFields } from './context-inheritance.ts';
|
||||
import { landStatusVisible } from './investment-land-preparation.ts';
|
||||
import { csvCell } from './csv.js';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
import { isApiSimulationResultSnapshotV1 } from './simulation-contract.ts';
|
||||
@@ -738,7 +739,7 @@ export const simulationFieldGroups: Record<
|
||||
label: 'Situação do terreno',
|
||||
options: 'land_status',
|
||||
showWhen: (d) =>
|
||||
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
|
||||
landStatusVisible(d) && (d.purpose !== 'simulate_only' || isInvestorProjectJourney(d)),
|
||||
},
|
||||
{
|
||||
key: 'area_required',
|
||||
|
||||
@@ -30,6 +30,39 @@ class InvestmentCorridorContextTest extends TestCase
|
||||
$entry=collect(app(CorridorAgribusinessOpportunities::class)->entries())->firstWhere('corridor',config('investment_corridors.'.$corridor.'.name'));
|
||||
return Opportunity::where('management_creation_key',$entry['key'])->firstOrFail();
|
||||
}
|
||||
public function test_land_status_uses_the_real_decision_preserves_inactive_values_and_allows_incomplete_drafts(): void
|
||||
{
|
||||
$id=$this->postJson('/investimentos',['title'=>'QA situação do terreno','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->assertCreated()->json('id');
|
||||
$setNeed=function(string $need) use($id): array {
|
||||
$version=$this->getJson('/investimentos/'.$id.'/contexto')->json('case.version');
|
||||
$this->postJson('/investimentos/'.$id.'/aplicabilidade',['version'=>$version,'land_needed'=>$need,'external_funding'=>'unknown','viability'=>'unknown','reason'=>'Cenário QA '.$need])->assertOk();
|
||||
return $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
|
||||
};
|
||||
$view=$setNeed('yes');
|
||||
$body=['context'=>$id,'personal_snapshot'=>[],'economic_snapshot'=>['purpose'=>'simulate_only','context_applicability_snapshot'=>$view['prefill']['simulator']['context_applicability_snapshot'],'land_status'=>'duat','selection_labels'=>['land_status'=>'DUAT']]];
|
||||
$created=$this->postJson('/api/v1/simulation-drafts',$body,['Idempotency-Key'=>(string)Str::uuid()])->assertCreated();
|
||||
$draft=$created->json('data.id');$etag=$created->headers->get('ETag');
|
||||
foreach(['no','unknown'] as $need){
|
||||
$view=$setNeed($need);
|
||||
$this->assertSame($need,json_decode($view['prefill']['simulator']['context_applicability_snapshot'],true)['land_preparation']['need']);
|
||||
$update=$this->putJson('/api/v1/simulation-drafts/'.$draft,['personal_snapshot'=>[],'economic_snapshot'=>['purpose'=>'simulate_only','context_applicability_snapshot'=>$body['economic_snapshot']['context_applicability_snapshot']]],['If-Match'=>$etag])->assertOk();
|
||||
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk();
|
||||
// Omitting context cannot bypass the real dossier decision. Hidden values are retained.
|
||||
$this->assertSame('duat',$recovered->json('data.economic_snapshot.land_status'));
|
||||
$this->assertSame('DUAT',$recovered->json('data.economic_snapshot.selection_labels.land_status'));
|
||||
$this->assertSame($need,json_decode($recovered->json('data.economic_snapshot.context_applicability_snapshot'),true)['land_preparation']['need']);
|
||||
$etag=$update->headers->get('ETag');
|
||||
}
|
||||
$setNeed('yes');
|
||||
$this->flushSession();$this->actingAs($this->investor);
|
||||
$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.land_status','duat');
|
||||
$updated=$this->putJson('/api/v1/simulation-drafts/'.$draft,[...$body,'economic_snapshot'=>['purpose'=>'simulate_only','land_status'=>'leased']],['If-Match'=>$etag])->assertOk();
|
||||
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.land_status','leased');
|
||||
$this->assertSame('yes',json_decode($recovered->json('data.economic_snapshot.context_applicability_snapshot'),true)['land_preparation']['need']);
|
||||
$this->putJson('/api/v1/simulation-drafts/'.$draft,[...$body,'economic_snapshot'=>['purpose'=>'simulate_only','land_status'=>'invalid']],['If-Match'=>$updated->headers->get('ETag')])->assertUnprocessable();
|
||||
$empty=app(\App\Services\InvestorContext::class)->prefill(['fields'=>['site_tenure'=>'lease'],'revision'=>0,'provenance'=>[],'land_preparation'=>['need'=>'yes','reason'=>'QA']]);
|
||||
$this->assertSame('leased',$empty['simulator']['land_status']);
|
||||
}
|
||||
public function test_simulator_reuses_names_for_the_exact_chosen_territory_and_sector_without_creating_a_location(): void
|
||||
{
|
||||
$id=$this->postJson('/investimentos',['title'=>'QA continuidade simulador','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->assertCreated()->json('id');
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { investmentLandPreparation, landStatusVisible } from '../../resources/js/lib/investment-land-preparation.ts';
|
||||
import { simulationFieldGroups, validateSimulationDraft, partitionSimulationDraft, restoreSimulationDraft } from '../../resources/js/lib/simulation-form.ts';
|
||||
import { inheritCompatibleFields } from '../../resources/js/lib/context-inheritance.ts';
|
||||
import { buildProjectDossierRows, exportProjectDossierCsv } from '../../resources/js/lib/project-dossier.ts';
|
||||
import { createProjectDossierReport, createProjectDossierPrintHtml } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
|
||||
const snapshot = (need) => JSON.stringify({version:1,context_revision:2,land_preparation:{need,reason:'Decisão guardada no dossier'}});
|
||||
const draft = (need) => ({purpose:'simulate_only',journey_type:'investor',land_status:'duat',land_status_label:'DUAT',context_applicability_snapshot:snapshot(need)});
|
||||
|
||||
test('no hides land status and yes restores the same saved value; unknown is a pending decision without a required answer', () => {
|
||||
const field=simulationFieldGroups.location.find(field=>field.key==='land_status');
|
||||
for(const need of ['no','yes','unknown']) {
|
||||
const input=draft(need);const before=JSON.stringify(input);
|
||||
assert.equal(field.showWhen(input),need==='yes');
|
||||
assert.equal(Boolean(validateSimulationDraft({...input,land_status:''},'location').land_status),false);
|
||||
const saved=partitionSimulationDraft(input);const recovered=restoreSimulationDraft(saved.personal_snapshot,saved.economic_snapshot);
|
||||
assert.equal(recovered.land_status,'duat');assert.equal(recovered.land_status_label,'DUAT');
|
||||
assert.equal(investmentLandPreparation(recovered.context_applicability_snapshot).need,need);
|
||||
assert.equal(JSON.stringify(input),before);
|
||||
}
|
||||
});
|
||||
|
||||
test('old snapshots use S10 and absent or malformed decisions never imply yes or no', () => {
|
||||
for(const [applicability,need] of [['applicable','yes'],['not_needed_for_preparation','no'],['undetermined','unknown']])
|
||||
assert.equal(investmentLandPreparation(JSON.stringify({version:1,path:{steps:[{code:'S10',applicability}]}})).need,need);
|
||||
for(const raw of ['', 'invalid', '{}', '{"version":1,"land_preparation":{"need":"invalid"}}'])
|
||||
assert.equal(investmentLandPreparation(raw).need,'unknown');
|
||||
assert.equal(landStatusVisible({}),true); // Standalone simulator preserves its previous behaviour.
|
||||
});
|
||||
|
||||
test('current dossier assessment refreshes after server recovery without overwriting the saved land answer', () => {
|
||||
const scope='dossier:A:simulator';
|
||||
const inherited=inheritCompatibleFields(scope,{}, {context_applicability_snapshot:snapshot('yes'),land_status:'duat'},null);
|
||||
const recovered={...inherited.patch,context_reuse:inherited.metadata,context_applicability_snapshot:snapshot('no')};
|
||||
const restored=inheritCompatibleFields(scope,recovered,{context_applicability_snapshot:snapshot('yes')},recovered.context_reuse);
|
||||
assert.equal(restored.patch.context_applicability_snapshot,snapshot('yes'));
|
||||
assert.equal(Object.hasOwn(restored.patch,'land_status'),false);
|
||||
});
|
||||
|
||||
test('report, CSV and print reflect yes, no and unknown without treating historical DUAT as current when hidden', async () => {
|
||||
for(const need of ['yes','no','unknown']) {
|
||||
const input=draft(need);const before=JSON.stringify(input);
|
||||
const rows=buildProjectDossierRows(input);
|
||||
assert.equal(rows.some(row=>row[1]==='Situação do terreno'),need==='yes');
|
||||
const report=createProjectDossierReport(input,{selectedSections:new Set(['location'])});
|
||||
const print=await createProjectDossierPrintHtml(report,[],false);
|
||||
const csv=exportProjectDossierCsv(input);
|
||||
if(need==='yes'){assert.match(print,/DUAT/);assert.match(csv,/DUAT/);}
|
||||
else {assert.doesNotMatch(print,/DUAT/);assert.doesNotMatch(csv,/DUAT/);assert.match(print,need==='no'?/valores anteriores conservados/:/Definir se precisa de terra/);}
|
||||
assert.equal(JSON.stringify(input),before);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user