Files
Investidor-V2/tests/Feature/DeskAttachmentFieldsTest.php
T

410 lines
29 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\VirtualDesk;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
final class DeskAttachmentFieldsTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private User $manager;
private string $caseId;
private string $id;
private string $edition;
protected function setUp(): void
{
parent::setUp();
$this->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());
}
}