151 lines
8.1 KiB
PHP
151 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Contact;
|
|
use App\Models\CrmTask;
|
|
use App\Models\InterestRequest;
|
|
use App\Models\Investor;
|
|
use App\Models\Lead;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
class CrmIntakeTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors;
|
|
use RefreshDatabase;
|
|
|
|
private function setupManager(): User
|
|
{
|
|
$user = User::factory()->create();
|
|
config(['portal.managers' => [$user->email]]);
|
|
$this->actingAsWithMfa($user);
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function interest(string $reference = 'IR-CRM-1'): InterestRequest
|
|
{
|
|
$interest = InterestRequest::create(['reference' => $reference, 'full_name' => 'Contacto fictício', 'email' => 'CONTACT@example.invalid',
|
|
'investor_type' => 'empresa', 'country' => 'Moçambique', 'sector_interest' => 'Energia', 'message' => 'Mensagem do pedido de teste.', 'privacy_consent' => true]);
|
|
DB::table('consents')->insert([
|
|
'id' => (string) Str::uuid(), 'source_system' => 'portal-intake', 'external_id' => 'CrmTestConsent:'.$reference,
|
|
'version_no' => 1, 'data_classification' => 'confidential', 'subject_ref' => $interest->reference,
|
|
'purpose' => 'respond_to_interest', 'status' => 'granted', 'granted_at' => now(),
|
|
'interest_request_id' => $interest->id, 'policy_version' => 'test-policy-v1',
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
return $interest;
|
|
}
|
|
|
|
private function data(User $user): array
|
|
{
|
|
return ['legal_name' => 'Empresa de teste', 'investor_type' => 'domestic', 'owner_user_id' => $user->id,
|
|
'title' => 'Lead de energia', 'next_action' => 'Rever informação', 'due_at' => now()->addDay()->toIso8601String()];
|
|
}
|
|
|
|
public function test_conversion_is_idempotent_and_preserves_source_with_owner_and_future_task(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$interest = $this->interest();
|
|
$data = $this->data($user);
|
|
$this->post('/management/crm/convert/'.$interest->id, $data)->assertRedirect();
|
|
$this->post('/management/crm/convert/'.$interest->id, $data)->assertRedirect();
|
|
$this->assertDatabaseCount('leads', 1);
|
|
$this->assertDatabaseCount('investors', 1);
|
|
$this->assertDatabaseCount('contacts', 1);
|
|
$this->assertDatabaseCount('tasks', 1);
|
|
$lead = Lead::firstOrFail();
|
|
$this->assertSame('portal:IR-CRM-1', $lead->first_source);
|
|
$this->assertSame($user->id, $lead->owner_user_id);
|
|
$this->assertSame('contact@example.invalid', Contact::firstOrFail()->email);
|
|
$this->assertSame($lead->id, CrmTask::firstOrFail()->lead_id);
|
|
$this->get('/management/crm/leads')->assertOk();
|
|
$this->get('/management/crm/leads/'.$lead->id)->assertOk();
|
|
$this->assertSame(4, DB::table('audit_events')->where('source_system', 'portal-management')->count());
|
|
}
|
|
|
|
public function test_existing_contact_requires_explicit_investor_selection_and_is_not_duplicated(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$first = $this->interest();
|
|
$this->post('/management/crm/convert/'.$first->id, $this->data($user))->assertRedirect();
|
|
$next = $this->interest('IR-CRM-2');
|
|
$this->post('/management/crm/convert/'.$next->id, $this->data($user))->assertSessionHasErrors('investor_id');
|
|
$this->assertDatabaseCount('leads', 1);
|
|
$this->post('/management/crm/convert/'.$next->id, [...$this->data($user), 'investor_id' => Investor::firstOrFail()->id])->assertRedirect();
|
|
$this->assertDatabaseCount('contacts', 1);
|
|
$this->assertDatabaseCount('investors', 1);
|
|
$this->assertDatabaseCount('leads', 2);
|
|
}
|
|
|
|
public function test_conversion_rechecks_the_canonical_contact_consent_record(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$interest = $this->interest();
|
|
DB::table('consents')->where('interest_request_id', $interest->id)
|
|
->where('purpose', 'respond_to_interest')
|
|
->update(['status' => 'withdrawn', 'withdrawn_at' => now(), 'updated_at' => now()]);
|
|
|
|
$this->post('/management/crm/convert/'.$interest->id, $this->data($user))
|
|
->assertSessionHasErrors('investor_id');
|
|
|
|
$this->assertDatabaseCount('leads', 0);
|
|
$this->assertDatabaseCount('investors', 0);
|
|
$this->assertDatabaseCount('contacts', 0);
|
|
$this->assertDatabaseCount('tasks', 0);
|
|
|
|
DB::table('consents')->where('interest_request_id', $interest->id)->update([
|
|
'status' => 'granted', 'withdrawn_at' => null, 'deleted_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
$this->post('/management/crm/convert/'.$interest->id, $this->data($user))
|
|
->assertSessionHasErrors('investor_id');
|
|
$this->assertDatabaseCount('leads', 0);
|
|
}
|
|
|
|
public function test_unauthorised_users_cannot_convert_and_input_requires_owner_and_future_action(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$interest = $this->interest();
|
|
$this->post('/management/crm/convert/'.$interest->id, [...$this->data($user), 'due_at' => now()->subDay()->toIso8601String()])->assertSessionHasErrors('due_at');
|
|
$this->actingAs(User::factory()->create())->post('/management/crm/convert/'.$interest->id, $this->data($user))->assertForbidden();
|
|
$this->assertDatabaseCount('leads', 0);
|
|
}
|
|
|
|
public function test_active_lead_keeps_a_next_action_and_qualification_is_audited(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$interest = $this->interest();
|
|
$this->post('/management/crm/convert/'.$interest->id, $this->data($user))->assertRedirect();
|
|
$lead = Lead::firstOrFail();
|
|
$task = CrmTask::firstOrFail();
|
|
$this->patch('/management/crm/leads/'.$lead->id.'/tasks/'.$task->id, ['version' => $task->fresh()->getAttribute('version_no'), 'status' => 'done'])->assertSessionHasErrors('status');
|
|
$this->post('/management/crm/leads/'.$lead->id.'/tasks', ['title' => 'Reunião', 'due_at' => now()->addDays(2)->toIso8601String(), 'assignee_user_id' => $user->id])->assertRedirect();
|
|
$this->patch('/management/crm/leads/'.$lead->id.'/tasks/'.$task->id, ['version' => $task->fresh()->getAttribute('version_no'), 'status' => 'done'])->assertRedirect();
|
|
$this->patch('/management/crm/leads/'.$lead->id, ['version' => $lead->fresh()->getAttribute('version_no'), 'status' => 'qualified', 'owner_user_id' => $user->id, 'qualification_reason' => 'Informação confirmada pelo gestor.'])->assertRedirect();
|
|
$this->assertSame('qualified', $lead->fresh()->status);
|
|
$this->assertDatabaseHas('audit_events', ['entity_id' => $lead->id, 'action' => 'updated']);
|
|
}
|
|
|
|
public function test_investor_edit_and_contact_duplicate_protection_are_scoped(): void
|
|
{
|
|
$user = $this->setupManager();
|
|
$interest = $this->interest();
|
|
$this->post('/management/crm/convert/'.$interest->id, $this->data($user))->assertRedirect();
|
|
$investor = Investor::firstOrFail();
|
|
$contact = Contact::firstOrFail();
|
|
$this->get('/management/crm/investors')->assertOk();
|
|
$this->get('/management/crm/investors/'.$investor->id)->assertOk();
|
|
$this->patch('/management/crm/investors/'.$investor->id, ['version' => $investor->fresh()->getAttribute('version_no'), 'legal_name' => 'Nome revisto', 'investor_type' => 'domestic', 'status' => 'active'])->assertRedirect();
|
|
$this->assertSame('Nome revisto', $investor->fresh()->legal_name);
|
|
$other = Contact::create(['full_name' => 'Outro', 'email' => 'other@example.invalid']);
|
|
$this->patch('/management/crm/investors/'.$investor->id.'/contacts/'.$contact->id, ['version' => $contact->fresh()->getAttribute('version_no'), 'full_name' => 'Nome revisto', 'email' => 'OTHER@example.invalid'])->assertSessionHasErrors('email');
|
|
$this->patch('/management/crm/investors/'.$investor->id.'/contacts/'.$other->id, ['version' => $other->fresh()->getAttribute('version_no'), 'full_name' => 'Outro', 'email' => 'new@example.invalid'])->assertNotFound();
|
|
}
|
|
}
|