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

141 lines
8.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Contact;
use App\Models\CrmTask;
use App\Models\Investor;
use App\Models\Lead;
use App\Models\User;
use App\Services\PortalAudit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\Support\AuthenticatesQaActors;
use Tests\TestCase;
final class CrmRecordVersionsTest extends TestCase
{
use AuthenticatesQaActors;
use RefreshDatabase;
private User $first;
private User $second;
private Investor $investor;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$this->first = User::factory()->create();
$this->second = User::factory()->create();
config(['portal.managers' => [$this->first->email, $this->second->email]]);
$this->actingAsWithMfa($this->first);
$this->investor = Investor::create(['legal_name' => 'SYNTHETIC original investor', 'investor_type' => 'domestic', 'status' => 'prospect'])->fresh();
}
private function investorData(array $changes = []): array
{
return ['version' => 1, 'legal_name' => 'SYNTHETIC first edit', 'investor_type' => 'domestic', 'status' => 'active', ...$changes];
}
public function test_stale_investor_form_cannot_overwrite_a_more_recent_edit_and_audit_excludes_personal_text(): void
{
$url = '/management/crm/investors/'.$this->investor->id;
$this->patchJson($url, $this->investorData())->assertRedirect();
$this->actingAsWithMfa($this->second)->patchJson($url, $this->investorData(['legal_name' => 'SYNTHETIC stale edit']))->assertUnprocessable()->assertJsonValidationErrors('version');
$this->assertDatabaseHas('investors', ['id' => $this->investor->id, 'legal_name' => 'SYNTHETIC first edit', 'version_no' => 2]);
$events = DB::table('audit_events')->where('entity_type', 'investors')->where('entity_id', $this->investor->id)->where('action', 'updated')->get();
$this->assertCount(1, $events);
$this->assertSame(1, json_decode($events[0]->before_data, true)['values']['version_no']);
$this->assertSame(2, json_decode($events[0]->after_data, true)['values']['version_no']);
$this->assertStringNotContainsString('SYNTHETIC first edit', $events[0]->after_data);
}
public function test_shared_contact_has_one_version_across_investors_and_old_forms_do_not_restore_old_phone(): void
{
$contact = Contact::create(['full_name' => 'SYNTHETIC shared contact', 'email' => 'synthetic-shared@example.invalid', 'phone' => '111'])->fresh();
$other = Investor::create(['legal_name' => 'SYNTHETIC other investor', 'investor_type' => 'domestic', 'status' => 'prospect']);
foreach ([$this->investor->id, $other->id] as $id) {
DB::table('investor_contacts')->insert(['investor_id' => $id, 'contact_id' => $contact->id]);
}
$data = ['version' => 1, 'full_name' => 'SYNTHETIC shared contact', 'email' => 'SYNTHETIC-SHARED@example.invalid', 'phone' => '222'];
$this->patchJson('/management/crm/investors/'.$this->investor->id.'/contacts/'.$contact->id, $data)->assertRedirect();
$this->actingAsWithMfa($this->second)->patchJson('/management/crm/investors/'.$other->id.'/contacts/'.$contact->id, [...$data, 'phone' => '111', 'full_name' => 'SYNTHETIC stale name'])->assertUnprocessable()->assertJsonValidationErrors('version');
$this->assertDatabaseHas('contacts', ['id' => $contact->id, 'phone' => '222', 'email' => 'synthetic-shared@example.invalid', 'version_no' => 2]);
$this->get('/management/crm/investors/'.$other->id)->assertOk()->assertInertia(fn ($page) => $page->where('contacts.0.version_no', 2)->where('contacts.0.phone', '222'));
}
public function test_lead_and_task_edits_are_versioned_without_bypassing_the_next_action_rule(): void
{
$lead = Lead::create(['investor_id' => $this->investor->id, 'owner_user_id' => $this->first->id, 'title' => 'SYNTHETIC lead', 'status' => 'new'])->fresh();
$task = CrmTask::create(['lead_id' => $lead->id, 'title' => 'SYNTHETIC next action', 'status' => 'open', 'due_at' => now()->addDay(), 'assignee_user_id' => $this->first->id])->fresh();
$taskUrl = '/management/crm/leads/'.$lead->id.'/tasks/'.$task->id;
$this->patchJson($taskUrl, ['version' => 1, 'status' => 'done'])->assertUnprocessable()->assertJsonValidationErrors('status');
$this->patchJson($taskUrl, ['version' => 1, 'status' => 'in_progress'])->assertRedirect();
CrmTask::create(['lead_id' => $lead->id, 'title' => 'SYNTHETIC alternative next action', 'status' => 'open', 'due_at' => now()->addDays(2), 'assignee_user_id' => $this->first->id]);
$this->patchJson($taskUrl, ['version' => 1, 'status' => 'done'])->assertUnprocessable()->assertJsonValidationErrors('version');
$data = ['version' => 1, 'owner_user_id' => $this->first->id, 'status' => 'qualified', 'qualification_reason' => 'SYNTHETIC qualified after internal review'];
$this->patchJson('/management/crm/leads/'.$lead->id, $data)->assertRedirect();
$this->actingAsWithMfa($this->second)->patchJson('/management/crm/leads/'.$lead->id, [...$data, 'status' => 'disqualified', 'qualification_reason' => 'SYNTHETIC stale qualification'])->assertUnprocessable()->assertJsonValidationErrors('version');
$this->assertDatabaseHas('leads', ['id' => $lead->id, 'status' => 'qualified', 'version_no' => 2]);
$this->assertDatabaseHas('tasks', ['id' => $task->id, 'status' => 'in_progress', 'version_no' => 2]);
}
public function test_missing_version_is_an_explicit_validation_error_and_does_not_change_the_record(): void
{
$data = $this->investorData();
unset($data['version']);
$this->patchJson('/management/crm/investors/'.$this->investor->id, $data)->assertUnprocessable()->assertJsonValidationErrors('version')->assertJsonPath('errors.version.0', 'Actualize os dados para recuperar a versão deste registo antes de gravar. As suas edições foram conservadas.');
$this->assertDatabaseHas('investors', ['id' => $this->investor->id, 'legal_name' => 'SYNTHETIC original investor', 'version_no' => 1]);
}
public function test_noop_does_not_create_another_revision_or_audit_and_metadata_cannot_be_reset_by_input(): void
{
$before = $this->investor->getAttributes();
$url = '/management/crm/investors/'.$this->investor->id;
$this->patchJson($url, ['version' => 1, 'legal_name' => $this->investor->legal_name, 'investor_type' => 'domestic', 'status' => 'prospect'])->assertRedirect();
$this->assertSame($before, $this->investor->fresh()->getAttributes());
$this->assertSame(0, DB::table('audit_events')->where('entity_id', $this->investor->id)->count());
$this->patchJson($url, [...$this->investorData(), 'version_no' => 0])->assertRedirect();
$this->assertSame(2, $this->investor->fresh()->version_no);
}
public function test_other_existing_crm_writers_advance_the_revision_without_changing_their_authority(): void
{
PortalAudit::save($this->investor, ['status' => 'active'], $this->first->id);
$this->assertSame(2, $this->investor->fresh()->version_no);
$this->patchJson('/management/crm/investors/'.$this->investor->id, $this->investorData())->assertUnprocessable()->assertJsonValidationErrors('version');
}
public function test_document_edition_is_not_incremented_by_the_crm_revision_policy(): void
{
$id = (string) Str::uuid();
DB::table('documents')->insert(['id' => $id, 'version_no' => 7, 'source_system' => 'synthetic', 'file_name' => 'synthetic-source.txt', 'media_type' => 'text/plain', 'storage_key' => 'synthetic-no-binary-required/'.$id, 'sha256' => str_repeat('a', 64), 'status' => 'quarantined']);
$document = new class extends Model
{
protected $table = 'documents';
protected $guarded = [];
public $incrementing = false;
protected $keyType = 'string';
};
$document = $document->newQuery()->findOrFail($id);
PortalAudit::save($document, ['file_name' => 'synthetic-renamed.txt'], $this->first->id);
$this->assertDatabaseHas('documents', ['id' => $id, 'version_no' => 7, 'file_name' => 'synthetic-renamed.txt']);
}
public function test_a_valid_revision_does_not_grant_manager_authority(): void
{
$this->actingAs(User::factory()->create())->patchJson('/management/crm/investors/'.$this->investor->id, $this->investorData())->assertForbidden();
$this->assertDatabaseHas('investors', ['id' => $this->investor->id, 'version_no' => 1]);
$this->actingAs($this->second)->patchJson('/management/crm/investors/'.$this->investor->id, $this->investorData())->assertRedirect();
$this->assertDatabaseHas('investors', ['id' => $this->investor->id, 'version_no' => 2, 'legal_name' => 'SYNTHETIC first edit']);
}
}