423 lines
36 KiB
PHP
423 lines
36 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\InterestRequest;
|
|
use App\Models\Investor;
|
|
use App\Models\InvestorJourney;
|
|
use App\Models\SimulationFormDraft;
|
|
use App\Models\User;
|
|
use App\Services\VirtualDesk;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
class InvestorGuidanceTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors;
|
|
use RefreshDatabase;
|
|
|
|
private User $investor;
|
|
|
|
private User $manager;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
$identity = DB::selectOne('select current_database() as name, inet_server_port() as port');
|
|
$this->assertStringEndsWith('_test', $identity->name);
|
|
$this->assertSame(55441, (int) $identity->port);
|
|
$this->investor = User::factory()->create();
|
|
$this->manager = User::factory()->create();
|
|
config(['portal.managers' => [$this->manager->email], 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false]);
|
|
Http::fake();
|
|
}
|
|
|
|
private function dossier(array $extra = []): string
|
|
{
|
|
return $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers', ['title' => 'Ideia fictícia QA', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, ...$extra])->assertCreated()->json('id');
|
|
}
|
|
|
|
private function guidance(string $id): array
|
|
{
|
|
return $this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->json();
|
|
}
|
|
|
|
private function service(): string
|
|
{
|
|
return $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id');
|
|
}
|
|
|
|
public function test_desk_declared_origin_is_recovered_and_invalid_country_preserves_draft(): void
|
|
{
|
|
$case = $this->dossier();
|
|
$service = $this->service();
|
|
$id = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id');
|
|
$url = '/balcao-virtual/pedidos/'.$id;
|
|
$this->getJson($url)->assertOk()->assertJsonMissingPath('request.payload.declared_origin_country_code');
|
|
$this->putJson($url, ['version' => 1, 'payload' => ['title' => 'Preparação QA', 'declared_origin_country_code' => 'PT']])->assertOk();
|
|
$this->actingAs(User::factory()->create())->getJson($url)->assertForbidden();
|
|
$this->actingAs($this->investor)->getJson($url)->assertOk()->assertJsonPath('request.payload.declared_origin_country_code', 'PT')->assertJsonPath('request.status', 'draft');
|
|
$this->putJson($url, ['version' => 2, 'payload' => ['title' => 'Preparação QA', 'declared_origin_country_code' => 'ZZ']])->assertUnprocessable()->assertJsonValidationErrors('payload.declared_origin_country_code');
|
|
$this->getJson($url)->assertJsonPath('request.payload.declared_origin_country_code', 'PT')->assertJsonPath('request.version', 2);
|
|
$this->assertDatabaseCount('interest_requests', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_panel_creation_redirect_confirms_persistence_and_never_submits_a_request(): void
|
|
{
|
|
$payload = ['title' => 'Dossier de teste QA do painel', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true];
|
|
$response = $this->actingAs($this->investor)->post('/balcao-virtual/dossiers', $payload);
|
|
$case = DB::table('desk_cases')->where('client_key', $payload['client_key'])->first();
|
|
$this->assertNotNull($case);
|
|
$response->assertRedirect('/balcao-virtual/dossiers/'.$case->id)->assertSessionHas('success');
|
|
$this->post('/balcao-virtual/dossiers', $payload)->assertRedirect('/balcao-virtual/dossiers/'.$case->id);
|
|
$this->assertDatabaseCount('desk_cases', 1);
|
|
$this->assertDatabaseCount('desk_requests', 0);
|
|
$this->getJson('/balcao-virtual/painel')->assertJsonPath('cases.0.id', $case->id);
|
|
$this->getJson('/balcao-virtual/dossiers/'.$case->id)->assertJsonPath('case.title', $payload['title']);
|
|
$this->post('/balcao-virtual/dossiers', [...$payload, 'reuse_consent' => false, 'client_key' => (string) Str::uuid()])->assertSessionHasErrors('reuse_consent');
|
|
$this->assertDatabaseCount('desk_cases', 1);
|
|
}
|
|
|
|
public function test_message_kind_is_recorded_at_send_time_and_legacy_is_not_reinterpreted(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$service = $this->service();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id');
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Resposta original do responsável.', 'client_key' => (string) Str::uuid()])->assertOk();
|
|
config(['portal.managers' => []]);
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->assertJsonPath('messages.0.kind', 'responsible_response')->assertJsonPath('messages.0.name', $this->manager->name);
|
|
$historicalId = (string) Str::uuid();
|
|
DB::table('desk_messages')->insert(['id' => $historicalId, 'request_id' => $requestId, 'actor_id' => $this->investor->id,
|
|
'client_key' => (string) Str::uuid(), 'body' => Crypt::encryptString('Mensagem histórica sem tipo original.'), 'created_at' => now()->addSecond()]);
|
|
$messages = $this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->json('messages');
|
|
$this->assertSame('historical_unclassified', collect($messages)->firstWhere('id', $historicalId)['kind']);
|
|
$this->assertDatabaseCount('desk_messages', 2);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_link_monitor_expires_checks_and_records_human_evidence_without_external_calls(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$link = (string) Str::uuid();
|
|
DB::table('desk_institutional_links')->insert(['id' => $link, 'stage' => 'E10', 'entity' => 'Instituição sintética QA', 'purpose' => 'Preparação',
|
|
'url' => 'https://example.invalid/', 'contact' => null, 'source_reference' => 'Fixture QA', 'status' => 'active', 'verified_at' => now()->subDays(8)]);
|
|
$this->get('/balcao-virtual/encaminhar/'.$link.'?case_id='.$id)->assertRedirect('/balcao-virtual/dossiers/'.$id)->assertSessionHas('warning');
|
|
$path = '/management/balcao/ligacoes/'.$link.'/conferencia';
|
|
$data = ['version' => 1, 'available' => false, 'evidence' => 'Consulta manual QA identificou indisponibilidade.', 'contact' => 'Contacto sintético QA; fonte fixture'];
|
|
$this->postJson($path, $data)->assertForbidden();
|
|
$this->actingAs($this->manager)->postJson($path, $data)->assertOk();
|
|
$this->postJson($path, $data)->assertConflict();
|
|
$this->assertDatabaseHas('desk_institutional_links', ['id' => $link, 'status' => 'unavailable', 'verified_by' => $this->manager->id]);
|
|
$this->assertDatabaseHas('audit_events', ['action' => 'institutional_link_checked', 'actor_user_id' => $this->manager->id]);
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id)->assertOk()->assertJsonPath('links.0.monitor.available', false);
|
|
$this->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk();
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_complete_idea_request_response_and_internal_crm_task_reuses_existing_identity(): void
|
|
{
|
|
$existing = Investor::create(['legal_name' => 'Investidor QA', 'investor_type' => 'individual', 'status' => 'prospect']);
|
|
DB::table('investor_accounts')->insert(['user_id' => $this->investor->id, 'investor_id' => $existing->id]);
|
|
$project = (string) Str::uuid();
|
|
DB::table('projects')->insert(['id' => $project, 'code' => 'GUIDANCE-QA', 'name' => 'Projecto fictício', 'status' => 'draft']);
|
|
$dossier = (string) Str::uuid();
|
|
DB::table('investment_dossiers')->insert(['id' => $dossier, 'project_id' => $project, 'title' => 'Projecto QA', 'content' => '{}', 'source' => 'fixture']);
|
|
$journey = InvestorJourney::create(['user_id' => $this->investor->id, 'investor_id' => $existing->id, 'dossier_id' => $dossier, 'status' => 'started']);
|
|
$id = $this->dossier(['journey_id' => $journey->id]);
|
|
$this->assertDatabaseCount('investors', 1);
|
|
$this->assertDatabaseHas('desk_cases', ['id' => $id, 'investor_id' => $existing->id, 'project_id' => $project, 'journey_id' => $journey->id]);
|
|
$view = $this->guidance($id);
|
|
$this->assertCount(7, $view['phases']);
|
|
$this->assertDatabaseCount('tasks', 0);
|
|
$this->assertDatabaseCount('desk_requests', 0);
|
|
$service = $this->service();
|
|
$requestKey = (string) Str::uuid();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => $requestKey])->assertCreated()->json('id');
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => $requestKey])->assertJsonPath('id', $requestId);
|
|
$code = 'support_preparation';
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => $code, 'context_hash' => $view['fingerprint'], 'decision' => 'accepted', 'reason' => 'Pretendo preparar apoio.'])->assertOk();
|
|
$this->assertDatabaseCount('tasks', 0);
|
|
$taskInput = ['code' => $code, 'context_hash' => $view['fingerprint'], 'consent' => true, 'due_at' => now()->addDays(2)->toIso8601String(), 'request_id' => $requestId];
|
|
$taskId = $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', $taskInput)->assertCreated()->json('id');
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', $taskInput)->assertJsonPath('id', $taskId);
|
|
$this->assertDatabaseCount('tasks', 1);
|
|
$this->assertDatabaseHas('tasks', ['id' => $taskId, 'desk_case_id' => $id, 'desk_request_id' => $requestId, 'assignee_user_id' => $this->investor->id, 'status' => 'open', 'external_sync_status' => 'pending_authorization']);
|
|
$payload = ['title' => 'Projecto QA', 'description' => 'Preciso de orientação sobre preparação.', 'regime' => 'unknown', 'investment_amount' => '1000.00', 'currency' => 'MZN', 'own_finance' => '600.00', 'other_finance' => '400.00'];
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 1, 'payload' => $payload, 'submit' => false])->assertOk()->assertJsonPath('status', 'draft');
|
|
$this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.payload.description', $payload['description']);
|
|
$submit = ['version' => 2, 'payload' => $payload, 'submit' => true, 'consent' => true];
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, $submit)->assertOk()->assertJsonPath('status', 'submitted');
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, $submit)->assertOk();
|
|
$this->get('/balcao-virtual/pedidos/'.$requestId.'/recibo')->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 3, 'status' => 'reviewing', 'reason' => 'Autodecisão indevida.'])->assertForbidden();
|
|
$this->actingAsWithMfa($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/responsavel', ['version' => 3, 'analyst_id' => $this->manager->id, 'reason' => 'Atribuição real QA.'])->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 4, 'status' => 'reviewing', 'reason' => 'Análise interna iniciada.'])->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Resposta humana QA: indique as evidências de mercado.', 'client_key' => (string) Str::uuid()])->assertOk();
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('messages.0.kind', 'responsible_response');
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$taskId, ['version' => 1, 'status' => 'done', 'reason' => 'Pedido submetido e primeira resposta consultada.'])->assertOk();
|
|
$this->assertDatabaseHas('tasks', ['id' => $taskId, 'status' => 'done']);
|
|
$this->assertDatabaseHas('desk_events', ['case_id' => $id, 'action' => 'guidance_task_updated']);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_incomplete_context_corrections_stale_choices_and_no_external_services(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$view = $this->guidance($id);
|
|
$this->assertNotEmpty($view['phases'][1]['questions']);
|
|
$this->assertSame('not_configured', $view['external_ai']['status']);
|
|
$this->assertSame([], $view['consulted']);
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => 'market_preparation', 'context_hash' => $view['fingerprint'], 'decision' => 'rejected', 'reason' => 'Hipótese ainda sem evidência.'])->assertOk();
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Transformação', 'expected_start_date' => '2027-01-01', 'ready_date' => '2027-02-01']])->assertOk();
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Alteração obsoleta']])->assertConflict();
|
|
$new = $this->guidance($id);
|
|
$this->assertTrue($new['phases'][1]['recommendations'][0]['review']['stale']);
|
|
$this->assertSame('declared_contradiction', $new['phases'][5]['risks'][0]['classification']);
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', ['code' => 'market_preparation', 'context_hash' => $view['fingerprint'], 'consent' => true, 'due_at' => now()->addDay()->toIso8601String()])->assertConflict();
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/fontes', ['query' => 'Licenciamento'])->assertOk()->assertJsonPath('status', 'unavailable')->assertJsonPath('consulted', []);
|
|
$this->assertDatabaseCount('tasks', 0);
|
|
$this->get('/balcao-virtual/dossiers/'.$id)->assertOk();
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_private_linked_draft_is_not_shared_with_manager_and_is_recovered_for_owner(): void
|
|
{
|
|
$draft = SimulationFormDraft::create(['id' => (string) Str::uuid(), 'owner_user_id' => $this->investor->id, 'version_no' => 1,
|
|
'create_key_hash' => hash('sha256', 'qa-key'), 'create_payload_hash' => hash('sha256', 'qa-data'), 'snapshot_sha256' => hash('sha256', 'qa-data'),
|
|
'personal_snapshot' => SimulationFormDraft::encryptSnapshot(['name' => 'PRIVATE PERSONAL']),
|
|
'economic_snapshot' => SimulationFormDraft::encryptSnapshot(['project_summary' => 'Ideia guardada', 'market_assumption' => 'Hipótese, sem procura provada', 'business_plan_snapshot' => '{"version":999}'])]);
|
|
$id = $this->dossier(['simulation_id' => $draft->id]);
|
|
$owner = $this->guidance($id);
|
|
$this->assertSame('Ideia guardada', $owner['facts']['project_summary']);
|
|
$this->assertArrayNotHasKey('market_evidence', $owner['facts']);
|
|
$this->assertStringNotContainsString('PRIVATE PERSONAL', json_encode($owner));
|
|
$this->assertSame(1, $owner['consulted'][0]['version']);
|
|
$draftUrl = '/simulador-fiscal?draft='.$draft->id;
|
|
$this->assertSame($draftUrl, $owner['actions']['simulator']);
|
|
$this->assertSame($draftUrl, $owner['actions']['plan']);
|
|
$finance = collect($owner['phases'])->firstWhere('id', 'finance');
|
|
$this->assertSame($draftUrl, $finance['recommendations'][0]['href']);
|
|
$otherDraft = $draft->replicate();
|
|
$otherDraft->id = (string) Str::uuid();
|
|
$otherDraft->economic_snapshot = SimulationFormDraft::encryptSnapshot(['project_summary' => 'Outro projecto, não reutilizar']);
|
|
$otherDraft->save();
|
|
$this->getJson('/api/v1/simulation-drafts/'.$draft->id)->assertOk()->assertJsonPath('data.id', $draft->id)->assertJsonPath('data.economic_snapshot.project_summary', 'Ideia guardada');
|
|
$manager = $this->actingAs($this->manager)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->json();
|
|
$this->assertNull($manager['financial_draft']);
|
|
$this->assertSame([], $manager['consulted']);
|
|
$this->assertSame('/simulador-fiscal', $manager['actions']['simulator']);
|
|
$this->getJson('/api/v1/simulation-drafts/'.$draft->id)->assertNotFound();
|
|
$this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers', ['title' => 'Reutilização', 'simulation_id' => $draft->id, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertJsonPath('id', $id);
|
|
$this->assertDatabaseCount('desk_cases', 1);
|
|
$this->actingAs(User::factory()->create())->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertForbidden();
|
|
$this->postJson('/balcao-virtual/dossiers', ['title' => 'Tentativa', 'simulation_id' => $draft->id, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertNotFound();
|
|
}
|
|
|
|
public function test_request_constraints_and_draft_permissions_preserve_existing_flow(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$service = $this->service();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id');
|
|
$data = ['version' => 1, 'payload' => ['title' => 'QA', 'description' => 'Contradição declarada', 'investment_amount' => '1000.00', 'currency' => 'MZN', 'own_finance' => '1.00', 'other_finance' => '1.00'], 'submit' => true, 'consent' => true];
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, $data)->assertUnprocessable();
|
|
$this->assertDatabaseHas('desk_requests', ['id' => $requestId, 'status' => 'draft', 'version' => 1]);
|
|
$this->actingAs($this->manager)->putJson('/balcao-virtual/pedidos/'.$requestId, $data)->assertForbidden();
|
|
$this->actingAs(User::factory()->create())->getJson('/balcao-virtual/pedidos/'.$requestId)->assertForbidden();
|
|
}
|
|
|
|
public function test_clarification_resubmission_and_signed_internal_outcome_preserve_snapshots(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$service = $this->service();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id');
|
|
$payload = ['title' => 'QA', 'description' => 'Descrição inicial', 'regime' => 'unknown', 'investment_amount' => '1000.00', 'currency' => 'MZN'];
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 1, 'payload' => $payload, 'submit' => true, 'consent' => true])->assertOk();
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 2, 'status' => 'returned', 'reason' => 'Esclareça o mercado previsto.', 'editable_fields' => ['description']])->assertOk();
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.submitted_snapshot.payload.description', 'Descrição inicial');
|
|
$changed = [...$payload, 'title' => 'Alteração indevida', 'description' => 'Mercado declarado, evidência pendente.'];
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 3, 'payload' => $changed, 'submit' => true, 'consent' => true, 'reason' => 'Resposta ao esclarecimento.'])->assertUnprocessable();
|
|
$payload['description'] = 'Mercado declarado, evidência pendente.';
|
|
$this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 3, 'payload' => $payload, 'submit' => true, 'consent' => true, 'reason' => 'Resposta ao esclarecimento.'])->assertOk()->assertJsonPath('status', 'submitted');
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 4, 'status' => 'reviewing', 'reason' => 'Resposta analisada internamente.'])->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 5, 'status' => 'opinion', 'reason' => 'Parecer interno, sem licença oficial.'])->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 6, 'status' => 'accepted', 'reason' => 'Dossier aceite para acompanhamento interno.'])->assertOk();
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.status', 'accepted')->assertJsonCount(2, 'revisions');
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_manual_document_payment_and_communication_evidence_without_external_confirmation(): void
|
|
{
|
|
Storage::fake('local');
|
|
$id = $this->dossier();
|
|
$service = $this->service();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id');
|
|
$doc = (string) Str::uuid();
|
|
$link = (string) Str::uuid();
|
|
$bytes = 'Comprovativo fictício QA, sem valor oficial.';
|
|
Storage::disk('local')->put('qa-proof.txt', $bytes);
|
|
$hash = hash('sha256', $bytes);
|
|
DB::table('documents')->insert(['id' => $doc, 'source_system' => 'qa', 'file_name' => 'qa-proof.txt', 'media_type' => 'text/plain', 'storage_key' => 'qa-proof.txt', 'sha256' => $hash, 'size_bytes' => strlen($bytes), 'uploaded_by' => $this->investor->id, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $hash, 'data_classification' => 'confidential']);
|
|
DB::table('desk_documents')->insert(['id' => $link, 'case_id' => $id, 'document_id' => $doc, 'category' => 'proof', 'source_reference' => 'Fixture QA', 'linked_by' => $this->investor->id]);
|
|
$review = ['version' => 1, 'outcome' => 'internal_verified', 'what_checked' => 'Legibilidade e referência declarada.', 'evidence' => 'Documento QA e declaração do investidor.'];
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', $review)->assertForbidden();
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', $review)->assertOk();
|
|
$event = DB::table('desk_events')->where('action', 'document_reviewed')->first();
|
|
$this->assertSame($this->manager->id, (int) $event->actor_id);
|
|
$this->assertSame($hash, app(VirtualDesk::class)->decode($event->payload)['sha256']);
|
|
$this->assertDatabaseHas('documents', ['id' => $doc, 'status' => 'active']);
|
|
$payment = ['reference' => 'QA-MANUAL-1', 'amount' => '25.50', 'currency' => 'MZN', 'source_reference' => 'Valor declarado em comprovativo fictício, não taxa automática.', 'document_id' => $doc, 'consent' => true];
|
|
$this->actingAs($this->investor)->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos', $payment)->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos', $payment)->assertOk();
|
|
$this->assertDatabaseCount('desk_payments', 1);
|
|
$record = DB::table('desk_payments')->first();
|
|
$this->assertSame('pending', $record->status);
|
|
$check = ['version' => 1, 'status' => 'internal_verified', 'reason' => 'Conferida referência e valor no comprovativo.', 'evidence' => 'Comprovativo QA, não consulta bancária.'];
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos/'.$record->id.'/conferencia', $check)->assertForbidden();
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos/'.$record->id.'/conferencia', $check)->assertOk();
|
|
$this->assertDatabaseHas('desk_payments', ['id' => $record->id, 'status' => 'internal_verified', 'verified_by' => $this->manager->id]);
|
|
$communication = ['channel' => 'whatsapp', 'direction' => 'received', 'occurred_at' => now()->subMinute()->toIso8601String(), 'summary' => 'Comunicação fictícia registada manualmente.', 'evidence' => 'Referência declarada pelo responsável QA.', 'client_key' => (string) Str::uuid()];
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', $communication)->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', $communication)->assertOk();
|
|
$this->assertSame(1, DB::table('desk_events')->where('action', 'manual_communication_recorded')->count());
|
|
Storage::disk('local')->put('qa-proof.txt', 'Ficheiro alterado');
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', [...$review, 'version' => 2])->assertUnprocessable();
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_parallel_preparation_unavailable_link_and_ordered_contestable_recommendations(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/etapas', ['version' => 1, 'stage' => 'E10', 'status' => 'preparing', 'reason' => 'Preparar em paralelo com o registo.'])->assertOk();
|
|
$this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/etapas', ['version' => 2, 'stage' => 'E10', 'status' => 'completed', 'reason' => 'Conclusão sem suporte.', 'evidence' => 'Afirmação sem documento.'])->assertUnprocessable();
|
|
$view = $this->guidance($id);
|
|
foreach (['accepted', 'rejected', 'corrected'] as $decision) {
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => 'support_preparation', 'context_hash' => $view['fingerprint'], 'decision' => $decision, 'reason' => 'Revisão explícita QA.'])->assertOk();
|
|
}
|
|
$updated = $this->guidance($id);
|
|
$this->assertSame('corrected', $updated['phases'][6]['recommendations'][0]['review']['decision']);
|
|
$link = (string) Str::uuid();
|
|
DB::table('desk_institutional_links')->insert(['id' => $link, 'stage' => 'E10', 'entity' => 'Entidade QA', 'purpose' => 'Preparação', 'url' => 'https://example.invalid/', 'contact' => 'Contacto fictício QA', 'status' => 'pending', 'source_reference' => 'Fixture não confirmada']);
|
|
$this->get('/balcao-virtual/encaminhar/'.$link.'?case_id='.$id)->assertRedirect('/balcao-virtual/dossiers/'.$id)->assertSessionHas('warning');
|
|
$this->getJson('/balcao-virtual/dossiers/'.$id)->assertOk();
|
|
$this->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk();
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_organization_viewers_and_removed_members_keep_module_permissions(): void
|
|
{
|
|
$viewer = User::factory()->create();
|
|
$officer = User::factory()->create();
|
|
$organization = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $organization, 'name' => 'Organização fictícia QA', 'organization_type' => 'private']);
|
|
foreach ([[$this->investor, 'manager'], [$viewer, 'viewer'], [$officer, 'officer']] as [$user, $role]) {
|
|
DB::table('memberships')->insert(['user_id' => $user->id, 'organization_id' => $organization, 'role' => $role]);
|
|
}
|
|
$id = $this->dossier(['organization_id' => $organization]);
|
|
$service = $this->service();
|
|
$requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id');
|
|
$this->actingAs($viewer)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->assertJsonPath('can_write', false);
|
|
$this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Alteração indevida']])->assertForbidden();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Tentativa de escrita viewer.', 'client_key' => (string) Str::uuid()])->assertForbidden();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', ['channel' => 'phone', 'direction' => 'received', 'occurred_at' => now()->subMinute()->toIso8601String(), 'summary' => 'Tentativa de escrita viewer.', 'evidence' => 'Sem permissão.', 'client_key' => (string) Str::uuid()])->assertForbidden();
|
|
$this->actingAs($officer)->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Declaração autorizada da organização']])->assertOk();
|
|
$this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Mensagem original do membro investidor.', 'client_key' => (string) Str::uuid()])->assertOk();
|
|
DB::table('memberships')->where('organization_id', $organization)->where('user_id', $officer->id)->update(['deleted_at' => now()]);
|
|
$this->actingAs($this->manager)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->assertJsonPath('messages.0.kind', 'investor_message')->assertJsonPath('messages.0.name', $officer->name);
|
|
DB::table('memberships')->where('organization_id', $organization)->where('user_id', $this->investor->id)->update(['deleted_at' => now()]);
|
|
$this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id)->assertForbidden();
|
|
$this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertForbidden();
|
|
$this->getJson('/balcao-virtual/contextos')->assertJsonCount(0, 'cases');
|
|
}
|
|
|
|
public function test_pending_installation_preserves_legacy_desk_and_requires_authentication(): void
|
|
{
|
|
$this->mock(VirtualDesk::class)->shouldReceive('installed')->andReturn(false);
|
|
$this->actingAs($this->investor)->get('/balcao-virtual')->assertOk();
|
|
$this->getJson('/balcao-virtual/painel')->assertStatus(503);
|
|
$this->getJson('/management/balcao/fontes')->assertForbidden();
|
|
$this->actingAs($this->manager)->getJson('/management/balcao/fontes')->assertOk()->assertJsonStructure(['total', 'attachmentCount', 'forms']);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_installation_preview_shows_target_and_refuses_proofs_for_a_different_database(): void
|
|
{
|
|
$this->mock(VirtualDesk::class)->shouldReceive('installed')->andReturn(false);
|
|
$this->actingAs($this->investor)->getJson('/management/balcao-install')->assertForbidden();
|
|
$view = $this->actingAs($this->manager)->getJson('/management/balcao-install')->assertOk()
|
|
->assertJsonPath('target.database', DB::connection()->getDatabaseName())->assertJsonPath('backupVerified', false)
|
|
->assertJsonStructure(['pendingMigrations', 'databaseReady', 'scanner', 'rollback', 'compatibilityVerified'])->json();
|
|
$before = DB::table('migrations')->count();
|
|
$this->postJson('/management/balcao-install', ['hash' => $view['hash'], 'confirm' => true])->assertStatus(422);
|
|
$this->assertSame($before, DB::table('migrations')->count());
|
|
}
|
|
|
|
public function test_existing_support_request_and_crm_investor_are_associated_without_copying(): void
|
|
{
|
|
$canonical = Investor::create(['legal_name' => 'Investidor já acompanhado QA', 'investor_type' => 'individual', 'status' => 'active']);
|
|
$original = InterestRequest::create(['reference' => 'QA-EXISTING-SUPPORT', 'full_name' => 'Investidor QA', 'email' => 'private@example.invalid', 'investor_type' => 'individual', 'country' => 'Moçambique', 'sector_interest' => 'Energia QA', 'message' => 'Pedido original preservado.', 'privacy_consent' => true,
|
|
'investor_id' => $canonical->id, 'assigned_to' => $this->manager->id, 'next_action' => 'Conferir documentação no registo existente.', 'metadata' => ['portal_user_id' => $this->investor->id]]);
|
|
$id = $this->dossier(['interest_request_id' => $original->id]);
|
|
$this->assertDatabaseCount('interest_requests', 1);
|
|
$this->assertDatabaseCount('desk_requests', 0);
|
|
$this->assertDatabaseCount('investors', 1);
|
|
$this->assertDatabaseHas('desk_cases', ['id' => $id, 'interest_request_id' => $original->id, 'investor_id' => $canonical->id]);
|
|
$this->getJson('/balcao-virtual/dossiers/'.$id)->assertJsonPath('legacyRequest.reference', 'QA-EXISTING-SUPPORT')->assertJsonPath('legacyRequest.responsible_name', $this->manager->name)->assertJsonMissingPath('legacyRequest.email');
|
|
$this->postJson('/balcao-virtual/dossiers', ['title' => 'Reutilizar', 'interest_request_id' => $original->id, 'reuse_consent' => true, 'client_key' => (string) Str::uuid()])->assertJsonPath('id', $id);
|
|
$this->assertDatabaseCount('desk_cases', 1);
|
|
$this->actingAs(User::factory()->create())->postJson('/balcao-virtual/dossiers', ['title' => 'Sem autorização', 'interest_request_id' => $original->id, 'reuse_consent' => true, 'client_key' => (string) Str::uuid()])->assertNotFound();
|
|
}
|
|
|
|
public function test_guidance_evidence_tracks_scope_latest_review_hash_expiration_and_permissions(): void
|
|
{
|
|
$id = $this->dossier();
|
|
$initial = $this->guidance($id);
|
|
$this->assertSame('absent', $initial['phases'][2]['evidence'][0]['state']);
|
|
$doc = (string) Str::uuid();
|
|
$link = (string) Str::uuid();
|
|
$bytes = 'Evidencia ficticia V2 QA';
|
|
Storage::disk('local')->put('v2-location-proof.txt', $bytes);
|
|
$hash = hash('sha256', $bytes);
|
|
DB::table('documents')->insert(['id' => $doc, 'source_system' => 'qa', 'file_name' => 'v2-location-proof.txt', 'media_type' => 'text/plain', 'storage_key' => 'v2-location-proof.txt', 'sha256' => $hash, 'version_no' => 1, 'size_bytes' => strlen($bytes), 'uploaded_by' => $this->investor->id, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $hash, 'data_classification' => 'confidential']);
|
|
DB::table('desk_documents')->insert(['id' => $link, 'case_id' => $id, 'document_id' => $doc, 'category' => 'location_evidence', 'source_reference' => 'Fixture QA', 'linked_by' => $this->investor->id]);
|
|
$attached = $this->guidance($id);
|
|
$this->assertSame('attached_unchecked', $attached['phases'][2]['evidence'][0]['state']);
|
|
$this->assertNotSame($initial['fingerprint'], $attached['fingerprint']);
|
|
$review = ['version' => 1, 'outcome' => 'internal_verified', 'what_checked' => 'Acesso descrito no documento ficticio.', 'evidence' => 'Texto e hash da fixture QA.', 'supports' => ['location_evidence']];
|
|
$url = '/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia';
|
|
$this->postJson($url, $review)->assertForbidden();
|
|
$this->actingAs($this->manager)->postJson($url, $review)->assertOk();
|
|
$verified = $this->guidance($id);
|
|
$e = $verified['phases'][2]['evidence'][0];
|
|
$this->assertSame('internally_verified', $e['state']);
|
|
$this->assertSame($this->manager->name, $e['documents'][0]['review']['author']);
|
|
$this->assertNotEmpty($e['documents'][0]['review']['date']);
|
|
$this->assertSame($review['what_checked'], $e['documents'][0]['review']['what_checked']);
|
|
$this->assertSame([], $verified['phases'][2]['risks']);
|
|
$this->assertSame('absent', $verified['phases'][3]['evidence'][0]['state']);
|
|
$this->actingAs(User::factory()->create())->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertForbidden();
|
|
DB::table('desk_documents')->where('id', $link)->update(['valid_until' => now()->subDay()->toDateString()]);
|
|
$this->assertSame('expired', $this->guidance($id)['phases'][2]['evidence'][0]['state']);
|
|
DB::table('desk_documents')->where('id', $link)->update(['valid_until' => null]);
|
|
DB::table('documents')->where('id', $doc)->update(['version_no' => 2]);
|
|
$this->assertSame('superseded', $this->guidance($id)['phases'][2]['evidence'][0]['state']);
|
|
DB::table('documents')->where('id', $doc)->update(['version_no' => 1]);
|
|
Storage::disk('local')->put('v2-location-proof.txt', 'Bytes substituidos QA');
|
|
$this->assertSame('superseded', $this->guidance($id)['phases'][2]['evidence'][0]['state']);
|
|
Storage::disk('local')->put('v2-location-proof.txt', $bytes);
|
|
$this->actingAs($this->manager)->postJson($url, [...$review, 'version' => 2, 'outcome' => 'contradiction_identified'])->assertOk();
|
|
$this->assertSame('contradiction', $this->guidance($id)['phases'][2]['evidence'][0]['state']);
|
|
// A new conference with no scope must not resurrect an older verification.
|
|
$this->actingAs($this->manager)->postJson($url, [...$review, 'version' => 3, 'supports' => []])->assertOk();
|
|
$this->assertSame('attached_unchecked', $this->guidance($id)['phases'][2]['evidence'][0]['state']);
|
|
Http::assertNothingSent();
|
|
}
|
|
}
|