diff --git a/app/Http/Controllers/VirtualDeskController.php b/app/Http/Controllers/VirtualDeskController.php
index fecf0423..608d6b54 100644
--- a/app/Http/Controllers/VirtualDeskController.php
+++ b/app/Http/Controllers/VirtualDeskController.php
@@ -78,6 +78,23 @@ class VirtualDeskController extends Controller
}
public function showCase(Request $request, string $id, VirtualDesk $desk): JsonResponse|Response
+ {
+ $data = $this->caseViewData($request, $id, $desk);
+
+ return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/case', $data);
+ }
+
+ public function preDossier(Request $request, string $id, VirtualDesk $desk): \Illuminate\Http\Response
+ {
+ return response()->view('desk.pre-dossier', $this->caseViewData($request, $id, $desk), 200, [
+ 'Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff',
+ ]);
+ }
+
+ /** Same authorised projection for the workspace and its preparation document.
+ * @return array
+ */
+ private function caseViewData(Request $request, string $id, VirtualDesk $desk): array
{
$case = $desk->case($request->user(), $id);
$owner = $desk->owns($request->user(), $case, false);
@@ -98,7 +115,7 @@ class VirtualDeskController extends Controller
'links' => DB::table('desk_institutional_links')->get()->map(fn ($link) => [...(array) $link, 'monitor' => app(InstitutionalLinkMonitor::class)->status($link)]), 'source' => config('virtual-desk.source'), 'support' => config('portal.support'),
'guidance' => app(InvestorGuidance::class)->installed() ? app(InvestorGuidance::class)->view($request->user(), $id) : null];
- return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/case', $data);
+ return $data;
}
/** @return Collection */
diff --git a/resources/js/pages/desk/case.tsx b/resources/js/pages/desk/case.tsx
index 26de9147..163baf5a 100644
--- a/resources/js/pages/desk/case.tsx
+++ b/resources/js/pages/desk/case.tsx
@@ -260,6 +260,15 @@ export default function Case({
: 'por associar'}
.
+
+ Pré-dossiê dos dados guardados — imprimir/guardar PDF (novo
+ separador)
+
{guidance ? (
+
+
+
+
+
+ Pré-dossiê — {{ $case['title'] }} — InvesteMoz
+
+
+
+
+ Imprimir / guardar como PDF
+ Voltar ao mesmo dossier
+
+
+ InvesteMoz · {{ config('portal.operator_name') ?: 'Tempo, Lda.' }}
+ Pré-dossiê de preparação do investimento
+ {{ $case['title'] }}
+ Dossier {{ $case['id'] }} · Versão {{ $case['version'] }} · {{ empty($case['closed_at']) ? 'Aberto' : 'Encerrado' }}
+ Consulta em {{ now('Africa/Maputo')->format('d/m/Y H:i') }} (CAT, UTC+2).
+
+ Documento de preparação com os dados actualmente acessíveis nesta conta. Não constitui aprovação de investimento, concessão de benefícios fiscais, licença, certificação da APIEX ou submissão oficial a uma entidade pública. As lacunas permanecem por preencher; a impressão não altera o dossier ou os seus pedidos.
+ @foreach(['profile' => 'Perfil declarado', 'company' => 'Entidade declarada', 'investment' => 'Investimento declarado'] as $key => $label)
+ @if(isset($case['payload'][$key]))
+ {{ $label }} {{ is_array($case['payload'][$key]) ? json_encode($case['payload'][$key], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $case['payload'][$key] }}
+ @endif
+ @endforeach
+ @if(!empty($case['simulation_id']))
+ Preparação fiscal associada Referência: {{ $case['simulation_id'] }}. A associação identifica a preparação existente; não confirma regras, elegibilidade ou benefícios. Consulte as fontes e versões no simulador antes de utilizar um resultado.
+ @endif
+
+ Pedidos no mesmo Balcão
+ Referência Estado Versão
+ @forelse($requests as $item)
+ {{ $item->protocol ?: 'Rascunho — sem submissão' }} {{ $states[$item->status] ?? $item->status }} {{ $item->version }}
+ @empty
+ Ainda sem pedidos acessíveis neste dossier.
+ @endforelse
+
+
+
+ Documentos acessíveis
+ A relação não anexa ficheiros nem confirma validade jurídica ou aprovação.
+ @forelse($documents as $document)
+ {{ $document->file_name ?? $document->title ?? 'Documento' }} · {{ $document->category ?? 'Categoria não indicada' }}
+ @empty
+ Ainda sem documentos acessíveis.
+ @endforelse
+
+
+
+
diff --git a/routes/virtual-desk.php b/routes/virtual-desk.php
index f1014fbf..98ae56d2 100644
--- a/routes/virtual-desk.php
+++ b/routes/virtual-desk.php
@@ -18,6 +18,7 @@ Route::middleware(['auth', 'verified', PrivateInvestmentResponse::class])->group
Route::get('contextos', [Guidance::class, 'contexts']);
Route::post('dossiers', [Desk::class, 'createCase']);
Route::get('dossiers/{id}', [Desk::class, 'showCase'])->whereUuid('id');
+ Route::get('dossiers/{id}/pre-dossier', [Desk::class, 'preDossier'])->whereUuid('id');
Route::put('dossiers/{id}/estado', [Desk::class, 'caseState'])->whereUuid('id');
Route::post('dossiers/{id}/etapas', [Desk::class, 'stage'])->whereUuid('id');
Route::post('dossiers/{id}/pedidos', [Desk::class, 'createRequest'])->whereUuid('id');
diff --git a/tests/Unit/DeskPreDossierTest.php b/tests/Unit/DeskPreDossierTest.php
new file mode 100644
index 00000000..b86699c3
--- /dev/null
+++ b/tests/Unit/DeskPreDossierTest.php
@@ -0,0 +1,63 @@
+never();
+ }
+
+ public function test_export_reuses_the_existing_case_authorisation_before_reading_any_data(): void
+ {
+ $actor = new User;
+ $actor->id = 9;
+ $id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
+ $request = Request::create('/balcao-virtual/dossiers/'.$id.'/pre-dossier');
+ $request->setUserResolver(fn () => $actor);
+ $desk = Mockery::mock(VirtualDesk::class);
+ $desk->shouldReceive('case')->once()->with($actor, $id)->andThrow(new HttpException(403));
+ $this->expectException(HttpException::class);
+ app(VirtualDeskController::class)->preDossier($request, $id, $desk);
+ }
+
+ public function test_preparation_document_escapes_declared_data_and_preserves_unknowns_and_internal_status(): void
+ {
+ $html = view('desk.pre-dossier', [
+ 'case' => ['id' => 'qa-case', 'title' => '', 'version' => 3, 'closed_at' => null,
+ 'payload' => ['profile' => ['name' => ' '], 'investment' => ['amount' => null]], 'simulation_id' => 'qa-existing-simulation'],
+ 'requests' => collect([(object) ['protocol' => null, 'status' => 'draft', 'version' => 1]]),
+ 'documents' => collect([(object) ['file_name' => '', 'category' => 'licença declarada']]),
+ 'states' => ['draft' => 'Rascunho'],
+ ])->render();
+ $this->assertStringNotContainsString('