CRM/backend/tests/Feature/ProfessionalCrmP2Test.php

156 خطوط
9.1 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Call;
use App\Models\Deal;
use App\Models\DealStage;
use App\Models\Lead;
use App\Models\Pipeline;
use App\Models\QualityReview;
use App\Models\Team;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProfessionalCrmP2Test extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
}
public function test_pipeline_board_is_scoped_and_stage_moves_are_transactional_versioned_and_audited(): void
{
[$supervisor, $agent] = $this->teamUsers('Sales');
[, $otherAgent] = $this->teamUsers('Other');
$pipeline = Pipeline::where('is_default', true)->firstOrFail();
$new = DealStage::where('pipeline_id', $pipeline->id)->where('slug', 'new')->firstOrFail();
$won = DealStage::where('pipeline_id', $pipeline->id)->where('is_won', true)->firstOrFail();
$deal = $this->deal($agent, $pipeline, $new, 'Scoped opportunity');
$this->deal($otherAgent, $pipeline, $new, 'Foreign opportunity');
$this->actingAs($supervisor)->getJson("/api/pipelines/{$pipeline->id}/board")
->assertOk()->assertJsonPath('summary.count', 1)->assertJsonFragment(['title' => 'Scoped opportunity'])->assertJsonMissing(['title' => 'Foreign opportunity']);
$this->actingAs($agent)->patchJson("/api/deals/{$deal->id}/stage", ['deal_stage_id' => $won->id, 'version' => 1])
->assertUnprocessable()->assertJsonValidationErrors('reason');
$this->actingAs($agent)->patchJson("/api/deals/{$deal->id}/stage", ['deal_stage_id' => $won->id, 'version' => 1, 'reason' => 'نیاز مشتری رفع شد', 'final_amount' => 950000])
->assertOk()->assertJsonPath('status', 'won')->assertJsonPath('version', 2);
$this->actingAs($agent)->patchJson("/api/deals/{$deal->id}/stage", ['deal_stage_id' => $new->id, 'version' => 1])
->assertUnprocessable()->assertJsonValidationErrors('version');
$this->assertDatabaseHas('deal_stage_histories', ['deal_id' => $deal->id, 'from_stage_id' => $new->id, 'to_stage_id' => $won->id, 'changed_by' => $agent->id]);
$this->assertDatabaseHas('activity_logs', ['action' => 'deal_stage_changed', 'subject_id' => $deal->id]);
$this->actingAs($supervisor)->getJson('/api/reports/operations')
->assertOk()
->assertJsonMissingPath('summary.won_value')
->assertJsonStructure(['summary' => ['open_leads', 'issued_invoices', 'invoiced_total', 'outstanding_total']]);
}
public function test_global_search_saved_views_and_preferences_are_scoped_to_user_and_team(): void
{
[$supervisor, $agent, $team] = $this->teamUsers('Search', true);
[, $otherAgent] = $this->teamUsers('Other');
Lead::create(['first_name' => 'سارا', 'last_name' => 'فروش', 'company' => 'شرکت آلفا', 'phone' => '02111111111', 'assigned_to' => $agent->id, 'team_id' => $team->id]);
Lead::create(['first_name' => 'سارا', 'last_name' => 'محرمانه', 'company' => 'شرکت دیگر', 'phone' => '02122222222', 'assigned_to' => $otherAgent->id]);
$this->actingAs($supervisor)->getJson('/api/global-search?q=سارا')->assertOk()->assertJsonCount(1, 'leads')->assertJsonPath('leads.0.last_name', 'فروش');
$created = $this->actingAs($supervisor)->postJson('/api/saved-views', [
'entity_type' => 'deal', 'name' => 'فرصت‌های داغ تیم', 'visibility' => 'team', 'team_id' => $team->id,
'filters' => ['forecast_category' => 'commit'], 'is_default' => true,
])->assertCreated();
$this->actingAs($agent)->getJson('/api/saved-views?entity_type=deal')->assertOk()->assertJsonFragment(['id' => $created->json('id'), 'name' => 'فرصت‌های داغ تیم']);
$this->actingAs($agent)->putJson('/api/workspace-preferences', [
'hidden_widgets' => ['team_performance'], 'widget_order' => ['task_summary'],
'notifications' => [['notification_type' => 'sla', 'in_app_enabled' => true, 'is_muted' => false]],
])->assertOk()->assertJsonPath('dashboard.hidden_widgets.0', 'team_performance');
}
public function test_scoring_sla_detection_and_automation_are_deterministic_and_idempotent(): void
{
$admin = $this->user('admin');
$agent = $this->user('agent');
$lead = Lead::create([
'first_name' => 'مینا', 'last_name' => 'گرم', 'company' => 'Intent Co', 'phone' => '02133333333',
'email' => 'mina@example.test', 'city' => 'تهران', 'product_interest' => 'CRM', 'priority' => 4,
'interest_level' => 'high', 'assigned_to' => $agent->id,
]);
Lead::whereKey($lead->id)->update(['created_at' => now()->subHours(3)]);
$lead->refresh();
$this->actingAs($agent)->postJson("/api/leads/{$lead->id}/score")->assertOk()->assertJsonPath('score_level', 'warm');
$rule = $this->actingAs($admin)->postJson('/api/sla-rules', [
'name' => 'اولین تماس دو ساعته', 'event' => 'first_contact', 'warning_minutes' => 60, 'breach_minutes' => 120, 'is_active' => true,
])->assertCreated();
$this->actingAs($admin)->postJson('/api/sla/detect')->assertOk()->assertJsonPath('created', 1);
$this->actingAs($admin)->postJson('/api/sla/detect')->assertOk()->assertJsonPath('created', 0);
$this->assertDatabaseHas('sla_breaches', ['sla_rule_id' => $rule->json('id'), 'breachable_id' => $lead->id, 'status' => 'breached']);
$automation = $this->actingAs($admin)->postJson('/api/automations', [
'name' => 'پیگیری خودکار لید', 'trigger' => 'manual', 'actions' => [['type' => 'create_task']], 'is_active' => true,
])->assertCreated();
$payload = ['entity_type' => 'lead', 'entity_id' => $lead->id, 'event_key' => 'test-lead-followup'];
$this->actingAs($admin)->postJson("/api/automations/{$automation->json('id')}/run", $payload)->assertStatus(202)->assertJsonPath('status', 'completed');
$this->actingAs($admin)->postJson("/api/automations/{$automation->json('id')}/run", $payload)->assertStatus(202);
$this->assertDatabaseCount('automation_runs', 1);
$this->assertDatabaseCount('tasks', 1);
}
public function test_custom_fields_are_typed_and_quality_review_can_only_be_acknowledged_by_its_agent(): void
{
$admin = $this->user('admin');
$agent = $this->user('agent');
$other = $this->user('agent');
$lead = Lead::create(['first_name' => 'علی', 'last_name' => 'کیفی', 'company' => 'QA', 'phone' => '02144444444', 'assigned_to' => $agent->id]);
$field = $this->actingAs($admin)->postJson('/api/custom-fields', [
'entity_type' => 'lead', 'key' => 'annual_budget', 'label' => 'بودجه سالانه', 'type' => 'number',
'is_active' => true, 'is_filterable' => true, 'is_searchable' => false, 'is_required' => false,
])->assertCreated();
$this->actingAs($agent)->putJson("/api/custom-field-values/lead/{$lead->id}", ['values' => ['annual_budget' => 1250000]])
->assertOk()->assertJsonPath('0.custom_field_definition_id', $field->json('id'))->assertJsonPath('0.value_number', '1250000.0000');
$call = Call::create(['lead_id' => $lead->id, 'user_id' => $agent->id, 'phone' => $lead->phone, 'result' => 'answered']);
$review = QualityReview::create([
'call_id' => $call->id, 'reviewer_id' => $admin->id, 'agent_id' => $agent->id, 'version' => 1, 'is_current' => true,
'overall_score' => 80, 'is_shared_with_agent' => true,
]);
$this->actingAs($other)->postJson("/api/quality-reviews/{$review->id}/acknowledge")->assertForbidden();
$this->actingAs($agent)->postJson("/api/quality-reviews/{$review->id}/acknowledge", ['agent_response' => 'دریافت شد'])
->assertOk()->assertJsonPath('status', 'acknowledged');
$this->assertNotNull($review->fresh()->acknowledged_at);
}
private function deal(User $owner, Pipeline $pipeline, DealStage $stage, string $title): Deal
{
return Deal::create([
'title' => $title, 'owner_id' => $owner->id, 'created_by' => $owner->id,
'pipeline_id' => $pipeline->id, 'deal_stage_id' => $stage->id,
'estimated_value' => 1000000, 'win_probability' => $stage->probability,
'sales_stage' => $stage->slug, 'status' => 'open', 'version' => 1,
]);
}
private function teamUsers(string $name, bool $returnTeam = false): array
{
$supervisor = $this->user('supervisor');
$agent = $this->user('agent');
$team = Team::create(['name' => $name, 'supervisor_id' => $supervisor->id, 'is_active' => true]);
$team->members()->attach([$supervisor->id, $agent->id]);
return $returnTeam ? [$supervisor, $agent, $team] : [$supervisor, $agent];
}
private function user(string $role): User
{
$user = User::factory()->create(['is_active' => true]);
$user->assignRole($role);
return $user;
}
}