214 lines
13 KiB
PHP
214 lines
13 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\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Tests\TestCase;
|
|
|
|
final class DeskPaymentEvidenceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $owner;
|
|
|
|
private User $reviewer;
|
|
|
|
private string $caseId;
|
|
|
|
private string $requestId;
|
|
|
|
private string $edition;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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();
|
|
}
|
|
}
|