From dcecd251020bd63b8977ad4a7028548824ea5ef5 Mon Sep 17 00:00:00 2001 From: nala Date: Tue, 10 Nov 2026 10:41:49 +0200 Subject: [PATCH] feat(desk): print authorized saved pre-dossier --- .../Controllers/VirtualDeskController.php | 19 +++++- resources/js/pages/desk/case.tsx | 9 +++ resources/views/desk/pre-dossier.blade.php | 66 +++++++++++++++++++ routes/virtual-desk.php | 1 + tests/Unit/DeskPreDossierTest.php | 63 ++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 resources/views/desk/pre-dossier.blade.php create mode 100644 tests/Unit/DeskPreDossierTest.php 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 + + +
+ +
+

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

+ + @forelse($requests as $item) + + @empty + + @endforelse +
ReferênciaEstadoVersão
{{ $item->protocol ?: 'Rascunho — sem submissão' }}{{ $states[$item->status] ?? $item->status }}{{ $item->version }}
Ainda sem pedidos acessíveis neste dossier.
+
+
+

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
+
+

Os dados em falta não foram substituídos por estimativas. Conserve a data e a versão desta cópia; alterações posteriores exigem nova consulta.

+
+ 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('