withoutVite(); Queue::fake(); Storage::fake('local'); $this->owner = User::factory()->create(); $this->reviewer = User::factory()->create(); config(['portal.managers' => [$this->reviewer->email]]); $this->edition = $this->actingAs($this->reviewer)->putJson('/management/balcao/servicos/project-change', ['base_version' => 0, 'definition' => config('virtual-desk.services.project-change'), 'reason' => 'SYNTHETIC payment evidence QA', 'confirm' => true])->assertCreated()->json('id'); $this->caseId = $this->actingAs($this->owner)->postJson('/balcao-virtual/dossiers', ['title' => 'SYNTHETIC payment dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->requestId = $this->newRequest(); } 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 upload(?string $request = null, string $name = 'synthetic-proof.txt'): array { return $this->post('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => DB::table('desk_cases')->where('id', $this->caseId)->value('version'), 'request_id' => $request, 'file' => UploadedFile::fake()->createWithContent($name, 'SYNTHETIC '.$name), 'category' => 'proof', 'source_reference' => 'SYNTHETIC documentary origin', 'consent' => true], ['Accept' => 'application/json'])->assertOk()->json(); } private function declare(array $changes = []): string { $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos', [...$this->declaration(), ...$changes])->assertOk(); return DB::table('desk_payments')->where('request_id', $this->requestId)->value('id'); } private function declaration(): array { return ['reference' => 'SYNTHETIC-REFERENCE-1', 'amount' => '25.50', 'currency' => 'MZN', 'source_reference' => 'SYNTHETIC amount and purpose, not a fee', 'consent' => true]; } private function operation(string $link, int $version = 1): array { return ['version' => $version, 'document_link_id' => $link, 'reason' => 'SYNTHETIC new documentary evidence', 'consent' => true, 'client_key' => (string) Str::uuid()]; } private function attach(string $payment, array $data): TestResponse { return $this->putJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos/'.$payment.'/comprovativo', $data); } private function clean(string $document): void { $hash = DB::table('documents')->where('id', $document)->value('sha256'); DB::table('documents')->where('id', $document)->update(['status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => trim($hash)]); } private function review(string $payment, int $version, string $status = 'internal_verified'): TestResponse { return $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos/'.$payment.'/conferencia', ['version' => $version, 'status' => $status, 'reason' => 'SYNTHETIC internal review only', 'evidence' => 'SYNTHETIC evidence, not a bank confirmation']); } public function test_pending_reference_can_receive_a_proof_without_a_second_declaration_or_changing_financial_facts(): void { $payment = $this->declare(); $uploaded = $this->upload(); $before = DB::table('desk_requests')->where('id', $this->requestId)->first(); $this->attach($payment, [...$this->operation($uploaded['link_id']), 'amount' => '999.00'])->assertOk(); $this->assertDatabaseCount('desk_payments', 1); $this->assertDatabaseCount('documents', 1); $this->assertCount(1, Storage::disk('local')->allFiles()); $this->assertDatabaseHas('desk_payments', ['id' => $payment, 'reference' => 'SYNTHETIC-REFERENCE-1', 'amount' => '25.50', 'currency' => 'MZN', 'source_reference' => 'SYNTHETIC amount and purpose, not a fee', 'status' => 'pending', 'version' => 2, 'verified_by' => null]); $after = DB::table('desk_requests')->where('id', $this->requestId)->first(); $this->assertSame($before->payload, $after->payload); $this->assertSame($before->version, $after->version); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('payments.0.document_evidence.link_id', $uploaded['link_id'])->assertJsonPath('payments.0.document_evidence.source_reference', 'SYNTHETIC documentary origin'); $this->assertSame(1, DB::table('desk_events')->where('action', 'payment_declared')->count()); $this->assertSame(1, DB::table('desk_events')->where('action', 'payment_document_linked')->count()); } public function test_replay_is_idempotent_and_changed_operation_or_stale_version_does_not_overwrite_evidence(): void { $payment = $this->declare(); $uploaded = $this->upload(); $operation = $this->operation($uploaded['link_id']); $this->attach($payment, $operation)->assertOk(); $this->attach($payment, [...$operation, 'version' => '1', 'consent' => '1'])->assertOk(); $this->attach($payment, [...$operation, 'reason' => 'SYNTHETIC changed operation'])->assertConflict(); $this->attach($payment, $this->operation($uploaded['link_id']))->assertConflict(); $this->assertDatabaseHas('desk_payments', ['id' => $payment, 'version' => 2]); $this->assertSame(1, DB::table('desk_events')->where('action', 'payment_document_linked')->count()); } public function test_selected_alias_is_frozen_and_another_requests_private_link_cannot_support_this_payment(): void { $other = $this->newRequest(); $uploaded = $this->upload($other); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos', [...$this->declaration(), 'document_link_id' => $uploaded['link_id']])->assertUnprocessable(); $document = $uploaded['documents'][0]['document_id']; $global = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'document_id' => $document, 'category' => 'proof', 'source_reference' => 'SYNTHETIC selected global source', 'valid_until' => '2028-02-03', 'consent' => true])->assertOk()->json('link_id'); $payment = $this->declare(['document_link_id' => $global]); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos', [...$this->declaration(), 'document_link_id' => $global])->assertOk(); DB::table('desk_documents')->where('id', $global)->update(['source_reference' => 'SYNTHETIC later edit', 'valid_until' => '2029-01-01']); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('payments.0.document_evidence.link_id', $global)->assertJsonPath('payments.0.document_evidence.source_reference', 'SYNTHETIC selected global source')->assertJsonPath('payments.0.document_evidence.valid_until', '2028-02-03'); $this->assertDatabaseHas('desk_payments', ['id' => $payment, 'document_id' => $document, 'version' => 1]); } public function test_issue_can_receive_corrected_support_but_a_verified_record_cannot_be_silently_replaced(): void { $uploaded = $this->upload(); $this->clean($uploaded['documents'][0]['document_id']); $payment = $this->declare(['document_link_id' => $uploaded['link_id']]); $this->actingAs($this->reviewer); $this->review($payment, 1)->assertOk(); $this->actingAs($this->owner); $this->attach($payment, $this->operation($uploaded['link_id'], 2))->assertConflict(); $this->actingAs($this->reviewer); $this->review($payment, 2, 'internal_issue')->assertOk(); $this->actingAs($this->owner); $new = $this->upload(null, 'synthetic-corrected.txt'); $this->attach($payment, $this->operation($new['link_id'], 3))->assertOk(); $this->assertDatabaseHas('desk_payments', ['id' => $payment, 'status' => 'pending', 'version' => 4, 'verified_by' => null, 'reason' => null, 'amount' => '25.50']); $this->assertSame(2, DB::table('desk_events')->where('action', 'payment_reviewed')->count()); $event = DB::table('desk_events')->where('action', 'payment_document_linked')->first(); $history = app(VirtualDesk::class)->decode($event->payload); $this->assertSame($uploaded['link_id'], $history['previous_document_evidence']['link_id']); } public function test_pending_scan_and_changed_original_are_not_verified_against_the_old_snapshot(): void { $uploaded = $this->upload(); $document = $uploaded['documents'][0]['document_id']; $payment = $this->declare(['document_link_id' => $uploaded['link_id']]); $this->actingAs($this->reviewer); $this->review($payment, 1)->assertUnprocessable(); $row = DB::table('documents')->where('id', $document)->first(); $bytes = 'SYNTHETIC replaced original'; Storage::disk('local')->put($row->storage_key, $bytes); DB::table('documents')->where('id', $document)->update(['sha256' => hash('sha256', $bytes), 'size_bytes' => strlen($bytes), 'version_no' => 2]); $this->clean($document); $this->review($payment, 1)->assertUnprocessable(); $this->actingAs($this->owner); $this->attach($payment, $this->operation($uploaded['link_id']))->assertOk(); $this->actingAs($this->reviewer); $this->review($payment, 2)->assertOk(); } public function test_legacy_payment_without_association_metadata_is_readable_and_keeps_its_historical_unknowns(): void { $uploaded = $this->upload(); $document = $uploaded['documents'][0]['document_id']; $payment = (string) Str::uuid(); DB::table('desk_payments')->insert(['id' => $payment, 'request_id' => $this->requestId, 'reference' => 'SYNTHETIC-REFERENCE-1', 'amount' => '25.50', 'currency' => 'MZN', 'source_reference' => $this->declaration()['source_reference'], 'document_id' => $document, 'status' => 'pending']); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('payments.0.document_evidence', null); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos', [...$this->declaration(), 'document_id' => $document])->assertOk(); $this->assertSame(0, DB::table('desk_events')->where('action', 'payment_declared')->count()); $this->clean($document); $this->actingAs($this->reviewer); $this->review($payment, 1)->assertOk(); } public function test_only_the_effective_owner_can_attach_evidence_and_review_remains_independent(): void { $uploaded = $this->upload(); $payment = $this->declare(); $this->review($payment, 1, 'internal_issue')->assertForbidden(); $this->actingAs($this->reviewer); $this->attach($payment, $this->operation($uploaded['link_id']))->assertForbidden(); $this->actingAs(User::factory()->create()); $this->attach($payment, $this->operation($uploaded['link_id']))->assertForbidden(); $this->actingAs($this->owner); $this->attach($payment, $this->operation((string) Str::uuid()))->assertUnprocessable(); DB::table('documents')->where('id', $uploaded['documents'][0]['document_id'])->update(['source_system' => 'opportunity-resources']); $this->attach($payment, $this->operation($uploaded['link_id']))->assertForbidden(); } public function test_closed_dossier_keeps_payment_history_readable_and_rejects_all_manual_writes(): void { $uploaded = $this->upload(); $payment = $this->declare(); DB::table('desk_cases')->where('id', $this->caseId)->update(['closed_at' => now()]); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('operationsEditable', false)->assertJsonCount(1, 'payments'); $this->attach($payment, $this->operation($uploaded['link_id']))->assertConflict(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/pagamentos', $this->declaration())->assertConflict(); $this->actingAs($this->reviewer); $this->review($payment, 1, 'internal_issue')->assertConflict(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/comunicacoes', ['channel' => 'phone', 'direction' => 'received', 'occurred_at' => now()->subMinute()->toIso8601String(), 'summary' => 'SYNTHETIC historical communication', 'evidence' => 'SYNTHETIC reference', 'client_key' => (string) Str::uuid()])->assertConflict(); } }