withoutVite(); Storage::fake('local'); Queue::fake(); $this->owner = User::factory()->create(); $this->manager = User::factory()->create(); config(['portal.managers' => [$this->manager->email]]); $this->edition = $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', $this->publication())->assertCreated()->json('id'); $this->caseId = $this->actingAs($this->owner)->postJson('/balcao-virtual/dossiers', ['title' => 'SYNTHETIC attachments', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->id = $this->newRequest(); } private function publication(int $base = 0): array { $definition = config('virtual-desk.services.project-change'); $definition['fields'] = [...$definition['fields'], 'branch' => ['label' => 'SYNTHETIC branch', 'type' => 'select', 'required' => false, 'options' => ['yes' => 'Yes', 'no' => 'No']], 'support' => ['label' => 'SYNTHETIC required support', 'type' => 'attachment', 'required' => true, 'mime_types' => ['text/plain'], 'max_size_kb' => 2, 'max_files' => 2, 'when' => ['field' => 'branch', 'equals' => 'yes']], 'optional_support' => ['label' => 'SYNTHETIC optional support', 'type' => 'attachment', 'required' => false, 'mime_types' => ['text/plain'], 'max_size_kb' => 2, 'max_files' => 1], ]; return ['base_version' => $base, 'definition' => $definition, 'confirm' => true, 'reason' => 'SYNTHETIC attachment field QA, not an actual publication.']; } private function newRequest(): string { return $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/pedidos', ['service_version_id' => $this->edition, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); } private function answers(array $support = []): array { return ['title' => 'SYNTHETIC submission', 'description' => 'SYNTHETIC complete private description', 'original_reference' => 'SYNTHETIC-REF', 'change_reason' => 'SYNTHETIC change', 'branch' => 'yes', 'support' => $support]; } private function upload(string $bytes = 'SYNTHETIC original bytes', ?string $requestId = null, ?string $field = 'support'): array { return $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => DB::table('desk_cases')->where('id', $this->caseId)->value('version'), 'request_id' => $requestId ?? $this->id, 'field_key' => $field, 'file' => UploadedFile::fake()->createWithContent('synthetic-proof.txt', $bytes), 'source_reference' => 'SYNTHETIC source only', 'category' => 'SYNTHETIC evidence', 'consent' => true], ['Accept' => 'application/json'])->assertOk()->json(); } private function clean(string $link): \stdClass { $id = DB::table('desk_documents')->where('id', $link)->value('document_id'); $document = DB::table('documents')->where('id', $id)->first(); DB::table('documents')->where('id', $id)->update(['status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => trim($document->sha256)]); return DB::table('documents')->where('id', $id)->first(); } private function save(array $payload, int $version = 1, bool $submit = true): TestResponse { return $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => $version, 'payload' => $payload, 'submit' => $submit, 'consent' => $submit]); } public function test_required_attachment_pending_scan_draft_and_checkpoint_are_preserved_but_submission_requires_a_usable_original(): void { $this->save($this->answers())->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $uploaded = $this->upload(); $link = $uploaded['link_id']; $payload = $this->answers([$link]); $this->assertCount(1, $uploaded['documents']); $this->assertSame(2, $uploaded['case_version']); $this->save($payload, 1, false)->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$this->id.'/rascunho', ['version' => 0, 'request_version' => 2, 'operation_key' => (string) Str::uuid(), 'payload' => $payload, 'edited_keys' => []])->assertOk(); $this->save($payload, 2)->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $document = $this->clean($link); $this->save($payload, 2)->assertOk()->assertJsonPath('status', 'submitted'); $snapshot = app(VirtualDesk::class)->decode(DB::table('desk_requests')->where('id', $this->id)->value('submitted_snapshot')); $this->assertTrue($snapshot['field_attachments']['support']['applicable']); $this->assertTrue($snapshot['field_attachments']['support']['files'][0]['usable_at_submission']); $this->assertSame(trim($document->sha256), $snapshot['field_attachments']['support']['files'][0]['sha256']); $this->assertSame($this->edition, $snapshot['field_attachments']['support']['service_version_id']); $this->get('/balcao-virtual/pedidos/'.$this->id.'/recibo?format=html')->assertOk()->assertSee('synthetic-proof.txt')->assertSee('Limites desta versão: 2 KB'); $this->actingAs(User::factory()->create())->get('/balcao-virtual/pedidos/'.$this->id.'/recibo?format=html')->assertForbidden(); } public function test_uploaded_and_reused_files_must_fit_the_pinned_field_limits(): void { $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'request_id' => $this->id, 'field_key' => 'support', 'file' => UploadedFile::fake()->createWithContent('oversized.txt', str_repeat('SYNTHETIC ', 400)), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertUnprocessable()->assertJsonValidationErrors('file'); $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='); $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'request_id' => $this->id, 'field_key' => 'support', 'file' => UploadedFile::fake()->createWithContent('synthetic.png', $png), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertUnprocessable()->assertJsonValidationErrors('file'); $this->assertDatabaseCount('documents', 0); $this->assertSame([], Storage::disk('local')->allFiles()); $upload = $this->upload(str_repeat('SYNTHETIC ', 400), null, null); $docId = $upload['documents'][0]['document_id']; $this->save($this->answers([$upload['link_id']]), 1, false)->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'request_id' => $this->id, 'field_key' => 'support', 'document_id' => $docId, 'consent' => true])->assertUnprocessable()->assertJsonValidationErrors('document_id'); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 1); } public function test_selection_rejects_scalar_duplicate_unknown_and_excessive_references(): void { $one = $this->upload('SYNTHETIC one')['link_id']; $two = $this->upload('SYNTHETIC two')['link_id']; $three = $this->upload('SYNTHETIC three')['link_id']; foreach ([$one, [$one, $one], [(string) Str::uuid()], [$one, $two, $three]] as $invalid) { $this->save([...$this->answers(), 'support' => $invalid], 1, false)->assertUnprocessable(); } $this->clean($one); $this->clean($two); $this->save($this->answers([$two, $one]))->assertOk(); $before = DB::table('desk_requests')->where('id', $this->id)->first(); $this->save($this->answers([$one, $two]))->assertOk(); $this->assertEquals($before, DB::table('desk_requests')->where('id', $this->id)->first()); } public function test_same_binary_can_be_linked_to_two_requests_without_copying_or_widening_the_previous_scope(): void { $one = $this->upload(); $other = $this->newRequest(); $two = $this->upload('SYNTHETIC original bytes', $other); $this->assertNotSame($one['link_id'], $two['link_id']); $this->assertSame($one['documents'][0]['document_id'], $two['documents'][0]['document_id']); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 2); $this->assertCount(1, Storage::disk('local')->allFiles()); $this->assertDatabaseHas('desk_documents', ['id' => $one['link_id'], 'request_id' => $this->id]); $this->assertDatabaseHas('desk_documents', ['id' => $two['link_id'], 'request_id' => $other]); $this->save($this->answers([$two['link_id']]), 1, false)->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $version = DB::table('desk_cases')->where('id', $this->caseId)->value('version'); $repeat = $this->upload(); $this->assertSame($one['link_id'], $repeat['link_id']); $this->assertSame($version, $repeat['case_version']); $this->assertDatabaseCount('desk_documents', 2); } public function test_authorised_reuse_uses_the_existing_reference_and_keeps_reviewer_access_within_the_new_link_scope(): void { $other = $this->newRequest(); $original = $this->upload('SYNTHETIC original bytes', $other); $document = $this->clean($original['link_id']); $response = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => $original['case_version'], 'request_id' => $this->id, 'field_key' => 'support', 'document_id' => $document->id, 'consent' => true])->assertOk(); $link = $response->json('link_id'); $this->assertNotSame($original['link_id'], $link); $this->assertDatabaseHas('desk_documents', ['id' => $link, 'source_reference' => 'SYNTHETIC source only', 'request_id' => $this->id]); $analyst = User::factory()->create(); DB::table('desk_requests')->where('id', $this->id)->update(['analyst_id' => $analyst->id]); $this->actingAs($analyst)->getJson('/balcao-virtual/pedidos/'.$this->id)->assertOk()->assertJsonCount(1, 'documents')->assertJsonCount(0, 'reusableDocuments'); $this->get('/balcao-virtual/dossiers/'.$this->caseId.'/documentos/'.$link)->assertOk(); $this->get('/balcao-virtual/dossiers/'.$this->caseId.'/documentos/'.$original['link_id'])->assertForbidden(); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => $response->json('case_version'), 'request_id' => $this->id, 'field_key' => 'support', 'document_id' => $document->id, 'consent' => true])->assertForbidden(); } public function test_global_support_is_reused_by_multiple_questions_without_new_upload_or_source_entry(): void { $global = $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'category' => 'SYNTHETIC global', 'file' => UploadedFile::fake()->createWithContent('synthetic-global.txt', 'SYNTHETIC shared bytes'), 'source_reference' => 'SYNTHETIC shared source', 'consent' => true], ['Accept' => 'application/json'])->assertOk()->json(); $this->clean($global['link_id']); $this->save([...$this->answers([$global['link_id']]), 'optional_support' => [$global['link_id']]])->assertOk(); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 1); $snapshot = app(VirtualDesk::class)->decode(DB::table('desk_requests')->where('id', $this->id)->value('submitted_snapshot')); $this->assertSame($snapshot['field_attachments']['support']['files'], $snapshot['field_attachments']['optional_support']['files']); } public function test_inactive_answers_are_preserved_without_becoming_required_or_claiming_applicable_evidence(): void { $unavailable = $this->upload(str_repeat('SYNTHETIC ', 400), null, null)['link_id']; $this->save([...$this->answers([$unavailable]), 'branch' => 'no'])->assertOk(); $snapshot = app(VirtualDesk::class)->decode(DB::table('desk_requests')->where('id', $this->id)->value('submitted_snapshot')); $this->assertFalse($snapshot['field_attachments']['support']['applicable']); $this->assertFalse($snapshot['field_attachments']['support']['files'][0]['usable_at_submission']); $this->get('/balcao-virtual/pedidos/'.$this->id.'/recibo?format=html')->assertOk()->assertSee('Resposta conservada de pergunta não aplicável'); } public function test_tampered_missing_and_inconsistent_originals_cannot_be_submitted(): void { $link = $this->upload()['link_id']; $document = $this->clean($link); $original = Storage::disk('local')->get($document->storage_key); Storage::disk('local')->put($document->storage_key, 'SYNTHETIC changed bytes'); $this->save($this->answers([$link]))->assertUnprocessable()->assertJsonValidationErrors('payload.support'); Storage::disk('local')->delete($document->storage_key); $this->save($this->answers([$link]))->assertUnprocessable(); Storage::disk('local')->put($document->storage_key, $original); DB::table('documents')->where('id', $document->id)->update(['size_bytes' => 1]); $this->save($this->answers([$link]))->assertUnprocessable(); $this->assertDatabaseHas('desk_requests', ['id' => $this->id, 'version' => 1, 'submitted_snapshot' => null]); } public function test_new_definition_does_not_change_old_limits_answers_or_receipt(): void { $one = $this->upload('SYNTHETIC one')['link_id']; $two = $this->upload('SYNTHETIC two')['link_id']; $this->clean($one); $this->clean($two); $this->save($this->answers([$one, $two]))->assertOk(); $before = DB::table('desk_requests')->where('id', $this->id)->first(); $input = $this->publication(1); $input['definition']['fields']['support']['max_files'] = 1; $input['definition']['fields']['support']['mime_types'] = ['application/pdf']; $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', $input)->assertCreated(); $this->actingAs($this->owner)->getJson('/balcao-virtual/pedidos/'.$this->id)->assertOk()->assertJsonPath('definition.fields.support.max_files', 2)->assertJsonPath('definition.fields.support.mime_types', ['text/plain']); $this->get('/balcao-virtual/pedidos/'.$this->id.'/recibo?format=html')->assertOk()->assertSee('2 ficheiro(s).'); $this->assertEquals($before, DB::table('desk_requests')->where('id', $this->id)->first()); } public function test_returned_field_scope_and_closed_or_submitted_state_are_enforced_for_uploads(): void { $link = $this->upload()['link_id']; $this->clean($link); $this->save($this->answers([$link]))->assertOk(); $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'request_id' => $this->id, 'field_key' => 'support', 'file' => UploadedFile::fake()->createWithContent('new.txt', 'SYNTHETIC new'), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertConflict(); app(VirtualDesk::class)->transition($this->manager, $this->id, ['version' => 2, 'status' => 'returned', 'reason' => 'SYNTHETIC support only', 'editable_fields' => ['support']]); $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'request_id' => $this->id, 'field_key' => 'optional_support', 'file' => UploadedFile::fake()->createWithContent('new.txt', 'SYNTHETIC new'), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertUnprocessable()->assertJsonValidationErrors('field_key'); $new = $this->upload('SYNTHETIC replacement')['link_id']; $this->clean($new); $this->putJson('/balcao-virtual/pedidos/'.$this->id, ['version' => 3, 'payload' => $this->answers([$new]), 'submit' => true, 'consent' => true, 'reason' => 'SYNTHETIC corrected support'])->assertOk(); $this->assertDatabaseCount('documents', 2); $this->assertDatabaseCount('desk_documents', 2); } public function test_definition_rejects_missing_or_inappropriate_file_limits_and_rollback_preserves_scoped_uses(): void { $input = $this->publication(1); foreach ([['max_size_kb' => 0], ['max_size_kb' => 10241], ['max_files' => 0], ['mime_types' => []], ['mime_types' => ['text/html']]] as $invalid) { $bad = $input; $bad['definition']['fields']['support'] = [...$bad['definition']['fields']['support'], ...$invalid]; $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', $bad)->assertUnprocessable(); } $bad = $input; unset($bad['definition']['fields']['support']['max_files']); $this->putJson('/management/balcao/servicos/project-change', $bad)->assertUnprocessable(); $bad = $input; $bad['definition']['fields']['title']['max_files'] = 1; $this->putJson('/management/balcao/servicos/project-change', $bad)->assertUnprocessable(); $this->actingAs($this->owner); $this->upload(); $other = $this->newRequest(); $this->upload('SYNTHETIC original bytes', $other); $migration = require database_path('migrations/2026_11_03_050000_scope_desk_document_links.php'); try { $migration->down(); $this->fail('Scoped links must not be destroyed.'); } catch (\RuntimeException $error) { $this->assertStringContainsString('Preserve', $error->getMessage()); } $this->assertDatabaseCount('desk_documents', 2); } public function test_organisation_representative_can_reuse_an_already_authorised_case_document_without_being_its_uploader(): void { $org = (string) Str::uuid(); $officer = User::factory()->create(); DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC document org', 'organization_type' => 'private']); foreach ([[$this->owner, 'admin'], [$officer, 'officer']] as [$actor,$role]) { DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'organization_id' => $org, 'user_id' => $actor->id, 'role' => $role]); } DB::table('desk_cases')->where('id', $this->caseId)->update(['organization_id' => $org]); $first = $this->upload(); $other = $this->newRequest(); $this->actingAs($officer)->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => $first['case_version'], 'request_id' => $other, 'field_key' => 'support', 'document_id' => $first['documents'][0]['document_id'], 'consent' => true])->assertOk(); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseHas('desk_documents', ['request_id' => $other, 'linked_by' => $officer->id, 'source_reference' => 'SYNTHETIC source only']); DB::table('memberships')->where('organization_id', $org)->where('user_id', $officer->id)->update(['deleted_at' => now()]); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 3, 'request_id' => $other, 'field_key' => 'support', 'document_id' => $first['documents'][0]['document_id'], 'consent' => true])->assertForbidden(); } public function test_two_link_aliases_cannot_count_as_two_documents_in_the_same_question(): void { $first = $this->upload(); $global = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => $first['case_version'], 'document_id' => $first['documents'][0]['document_id'], 'category' => 'SYNTHETIC dossier use', 'consent' => true])->assertOk()->json(); $this->save($this->answers([$first['link_id'], $global['link_id']]), 1, false)->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 2); } public function test_missing_scope_migration_is_a_specific_dependency_without_removing_manual_drafts(): void { DB::statement('ALTER INDEX desk_documents_scope_unique RENAME TO desk_documents_scope_qa_absent'); try { $input = $this->publication(1); $input['definition']['title'] = 'SYNTHETIC changed title'; $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', $input)->assertUnprocessable(); $this->actingAs($this->owner); $this->save(['title' => 'SYNTHETIC still editable'], 1, false)->assertOk(); $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'request_id' => $this->id, 'field_key' => 'support', 'file' => UploadedFile::fake()->createWithContent('new.txt', 'SYNTHETIC'), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertConflict(); } finally { DB::statement('ALTER INDEX desk_documents_scope_qa_absent RENAME TO desk_documents_scope_unique'); } $this->assertDatabaseCount('documents', 0); } public function test_encrypted_original_can_be_submitted_and_reviewed_with_the_shared_integrity_rules(): void { $link = $this->upload()['link_id']; $document = $this->clean($link); $bytes = Storage::disk('local')->get($document->storage_key); Storage::disk('local')->put($document->storage_key, Crypt::encryptString($bytes)); DB::table('documents')->where('id', $document->id)->update(['storage_encryption' => 'laravel']); $this->save($this->answers([$link]))->assertOk(); $this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos/'.$link.'/conferencia', ['version' => 2, 'request_id' => $this->id, 'outcome' => 'internal_verified', 'what_checked' => 'SYNTHETIC original bytes and scope', 'evidence' => 'SYNTHETIC review only', 'supports' => []])->assertOk(); $this->assertSame(1, DB::table('desk_events')->where('action', 'document_reviewed')->count()); Storage::disk('local')->put($document->storage_key, 'SYNTHETIC corrupt ciphertext'); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos/'.$link.'/conferencia', ['version' => 3, 'request_id' => $this->id, 'outcome' => 'internal_verified', 'what_checked' => 'SYNTHETIC repeat', 'evidence' => 'SYNTHETIC repeat', 'supports' => []])->assertUnprocessable(); $this->assertSame(1, DB::table('desk_events')->where('action', 'document_reviewed')->count()); } public function test_attachment_questions_with_financial_identifiers_do_not_become_numeric_answers_or_financing_questions(): void { $fields = []; $payload = []; foreach (['investment_amount', 'own_finance', 'other_finance'] as $key) { $fields[$key] = ['label' => 'SYNTHETIC evidence', 'type' => 'attachment', 'required' => false, 'mime_types' => ['text/plain'], 'max_files' => 1, 'max_size_kb' => 2]; $payload[$key] = [(string) Str::uuid()]; } $findings = app(VirtualDesk::class)->findings($payload, ['fields' => $fields]); $this->assertNotContains('finance_unknown', array_column($findings, 'code')); $this->assertNotContains('finance_difference', array_column($findings, 'code')); } public function test_unknown_legacy_file_metadata_is_measured_without_reupload_or_rewriting_the_original_record(): void { $link = $this->upload()['link_id']; $document = $this->clean($link); DB::table('documents')->where('id', $document->id)->update(['media_type' => '', 'size_bytes' => null]); $this->save($this->answers([$link]))->assertOk(); $snapshot = app(VirtualDesk::class)->decode(DB::table('desk_requests')->where('id', $this->id)->value('submitted_snapshot')); $file = $snapshot['field_attachments']['support']['files'][0]; $this->assertSame('text/plain', $file['media_type']); $this->assertSame($document->size_bytes, $file['size_bytes']); $this->assertTrue($file['metadata_measured']); $this->assertSame('', $file['recorded_media_type']); $this->assertNull($file['recorded_size_bytes']); $this->assertDatabaseHas('documents', ['id' => $document->id, 'media_type' => '', 'size_bytes' => null]); $this->assertDatabaseCount('documents', 1); } public function test_actual_file_format_cannot_be_replaced_by_a_misleading_registered_mime_type(): void { $link = $this->upload()['link_id']; $document = $this->clean($link); DB::table('documents')->where('id', $document->id)->update(['media_type' => 'application/pdf']); $input = $this->publication(1); $input['definition']['fields']['support']['mime_types'] = ['application/pdf']; $this->edition = $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', $input)->assertCreated()->json('id'); $this->actingAs($this->owner); $this->id = $this->newRequest(); $result = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'request_id' => $this->id, 'field_key' => 'support', 'document_id' => $document->id, 'consent' => true])->assertOk(); $this->save($this->answers([$result->json('link_id')]))->assertUnprocessable()->assertJsonValidationErrors('payload.support'); $this->assertDatabaseHas('desk_requests', ['id' => $this->id, 'version' => 1, 'submitted_snapshot' => null]); } public function test_owner_reuse_picker_preserves_the_chosen_source_and_declared_dates_without_exposing_other_request_inventory_to_reviewers(): void { $second = $this->newRequest(); $fromB = $this->upload('SYNTHETIC reused original', $second); $third = $this->newRequest(); $fromC = $this->upload('SYNTHETIC reused original', $third); DB::table('desk_documents')->where('id', $fromB['link_id'])->update(['source_reference' => 'SYNTHETIC ref B', 'stage' => 'E1', 'valid_until' => '2027-01-02']); DB::table('desk_documents')->where('id', $fromC['link_id'])->update(['source_reference' => 'SYNTHETIC ref C', 'stage' => 'E2', 'valid_until' => '2028-03-04']); $this->getJson('/balcao-virtual/pedidos/'.$this->id)->assertOk()->assertJsonCount(0, 'documents')->assertJsonCount(2, 'reusableDocuments'); $version = DB::table('desk_cases')->where('id', $this->caseId)->value('version'); $body = ['version' => $version, 'request_id' => $this->id, 'field_key' => 'support', 'document_id' => $fromB['documents'][0]['document_id'], 'reuse_link_id' => $fromB['link_id'], 'consent' => true]; $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', [...$body, 'reuse_link_id' => (string) Str::uuid()])->assertForbidden(); $result = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', $body)->assertOk(); $link = $result->json('link_id'); $this->assertDatabaseHas('desk_documents', ['id' => $link, 'request_id' => $this->id, 'source_reference' => 'SYNTHETIC ref B', 'valid_until' => '2027-01-02', 'stage' => 'E10']); $event = DB::table('desk_events')->where('request_id', $this->id)->where('action', 'document_linked')->orderByDesc('event_order')->first(); $trace = app(VirtualDesk::class)->decode($event->payload); $this->assertSame($fromB['link_id'], $trace['source_link_id']); $this->assertSame('E1', $trace['reused_source']['stage']); $this->assertArrayNotHasKey('request_id', $trace['reused_source']); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 3); $this->assertCount(1, Storage::disk('local')->allFiles()); } public function test_closed_dossier_keeps_request_readable_without_offering_edits_or_document_reuse(): void { DB::table('desk_cases')->where('id', $this->caseId)->update(['closed_at' => now()]); $this->getJson('/balcao-virtual/pedidos/'.$this->id)->assertOk()->assertJsonPath('request.status', 'draft')->assertJsonPath('fieldsEditable', false)->assertJsonCount(0, 'reusableDocuments'); $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'request_id' => $this->id, 'field_key' => 'support', 'file' => UploadedFile::fake()->createWithContent('synthetic.txt', 'SYNTHETIC'), 'source_reference' => 'SYNTHETIC', 'consent' => true], ['Accept' => 'application/json'])->assertConflict(); $this->assertDatabaseCount('documents', 0); $this->assertSame([], Storage::disk('local')->allFiles()); } }