makeUserWithRole('agent'); $lead = $this->makeLead($agent, [ 'priority' => 9, 'next_follow_up_at' => now()->subHour(), ]); $this->makePrimaryContact($lead, '09120001122'); FollowUp::create([ 'lead_id' => $lead->id, 'user_id' => $agent->id, 'scheduled_at' => now()->subHour(), 'status' => 'pending', ]); $response = $this->actingAs($agent) ->getJson('/api/agent/mobile/dashboard') ->assertOk() ->assertJsonPath('data.stats.leads_needing_action', 1) ->assertJsonPath('data.suggested_call.id', $lead->id); $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control')); $this->actingAs($agent) ->getJson('/api/agent/mobile/call-queue') ->assertOk() ->assertJsonPath('data.0.id', $lead->id) ->assertJsonMissing(['phone' => '09120001122']); } public function test_mobile_leads_are_scoped_to_logged_in_agent(): void { $agent = $this->makeUserWithRole('agent'); $otherAgent = $this->makeUserWithRole('agent'); $ownLead = $this->makeLead($agent, ['first_name' => 'Own']); $otherLead = $this->makeLead($otherAgent, ['first_name' => 'Other']); $this->actingAs($agent) ->getJson('/api/agent/mobile/leads') ->assertOk() ->assertJsonFragment(['id' => $ownLead->id]) ->assertJsonMissing(['id' => $otherLead->id]); $this->actingAs($agent) ->getJson("/api/agent/mobile/leads/{$otherLead->id}") ->assertForbidden(); } public function test_non_agent_cannot_access_mobile_agent_endpoints(): void { $supervisor = $this->makeUserWithRole('supervisor'); $this->actingAs($supervisor) ->getJson('/api/agent/mobile/dashboard') ->assertForbidden(); } public function test_mobile_lead_detail_does_not_expose_phone_numbers(): void { $agent = $this->makeUserWithRole('agent'); $lead = $this->makeLead($agent, [ 'phone' => '09123334455', 'phone_secondary' => '02199887766', ]); $this->makePrimaryContact($lead, '09123334455'); $response = $this->actingAs($agent) ->getJson("/api/agent/mobile/leads/{$lead->id}") ->assertOk(); $content = $response->getContent(); $this->assertStringNotContainsString('09123334455', $content); $this->assertStringNotContainsString('02199887766', $content); } public function test_mobile_start_call_uses_secure_endpoint_without_exposing_phone(): void { $agent = $this->makeUserWithRole('agent'); $lead = $this->makeLead($agent); $contact = $this->makePrimaryContact($lead, '09124445566'); $phone = $contact->phones()->first(); $response = $this->actingAs($agent) ->postJson('/api/agent/mobile/calls', [ 'lead_id' => $lead->id, 'contact_phone_id' => $phone->id, ]) ->assertCreated() ->assertJsonPath('data.contact_phone_id', $phone->id); $this->assertStringNotContainsString('09124445566', $response->getContent()); $response->assertJsonMissingPath('data.recording_url'); $response->assertJsonMissingPath('data.provider_call_id'); } public function test_ami_call_requires_agent_extension(): void { Setting::updateOrCreate(['key' => 'voip_provider'], ['value' => 'ami', 'group' => 'voip', 'type' => 'string']); $agent = $this->makeUserWithRole('agent'); $lead = $this->makeLead($agent); $contact = $this->makePrimaryContact($lead, '09124445566'); $phone = $contact->phones()->first(); $this->actingAs($agent) ->postJson('/api/agent/mobile/calls', [ 'lead_id' => $lead->id, 'contact_phone_id' => $phone->id, ]) ->assertUnprocessable() ->assertJsonPath('message', 'شماره داخلی برای این کاربر ثبت نشده است.'); $this->assertDatabaseMissing('calls', [ 'lead_id' => $lead->id, 'user_id' => $agent->id, ]); } public function test_mobile_call_result_creates_follow_up_new_contact_and_primary_change(): void { $agent = $this->makeUserWithRole('agent'); $lead = $this->makeLead($agent); $contact = $this->makePrimaryContact($lead, '09127778899'); $phone = $contact->phones()->first(); $callId = $this->actingAs($agent) ->postJson('/api/agent/mobile/calls', [ 'lead_id' => $lead->id, 'contact_phone_id' => $phone->id, ]) ->json('data.call_id'); $followUpAt = now()->addDay()->setSecond(0)->toIso8601String(); $this->actingAs($agent) ->postJson('/api/agent/mobile/call-results', [ 'call_id' => $callId, 'result' => 'معرفی شماره جدید', 'notes' => 'با مخاطب جدید هماهنگ شود', 'referral' => [ 'name' => 'خانم محمدی', 'phone' => '09121110000', 'role' => 'مدیر خرید', 'relation_description' => 'همکار تصمیم‌گیرنده', 'make_primary' => true, 'next_follow_up_at' => $followUpAt, ], ]) ->assertOk() ->assertJsonPath('message', 'نتیجه تماس ثبت شد'); $this->assertDatabaseHas('calls', [ 'id' => $callId, 'result' => 'معرفی شماره جدید', 'user_id' => $agent->id, ]); $this->assertDatabaseHas('follow_ups', [ 'lead_id' => $lead->id, 'user_id' => $agent->id, 'call_id' => $callId, 'status' => 'pending', ]); $this->assertDatabaseHas('contacts', [ 'lead_id' => $lead->id, 'name' => 'خانم محمدی', 'is_primary' => true, ]); $this->assertDatabaseHas('contact_phones', [ 'phone' => '09121110000', 'status' => 'active', ]); $this->assertSame(1, ContactRelation::where('lead_id', $lead->id)->where('from_contact_id', $contact->id)->count()); $this->assertFalse($contact->fresh()->is_primary); } private function makeUserWithRole(string $roleName): User { $role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => 'web']); $user = User::factory()->create(['is_active' => true]); $user->assignRole($role); return $user; } private function makeLead(User $agent, array $attributes = []): Lead { $status = LeadStatus::firstOrCreate( ['name' => 'در جریان'], ['color' => '#2563EB', 'sort_order' => 0, 'is_default' => true] ); $stage = PipelineStage::firstOrCreate( ['name' => 'در انتظار تماس'], ['color' => '#6366F1', 'sort_order' => 1, 'is_default' => false] ); return Lead::create(array_merge([ 'company' => 'Acme', 'first_name' => 'Ali', 'last_name' => 'Karimi', 'phone' => '02111112222', 'assigned_to' => $agent->id, 'is_unassigned' => false, 'lead_status_id' => $status->id, 'pipeline_stage_id' => $stage->id, ], $attributes)); } private function makePrimaryContact(Lead $lead, string $phone): Contact { $contact = Contact::create([ 'lead_id' => $lead->id, 'name' => 'آقای رضایی', 'role' => 'مسئول خرید', 'status' => 'active', 'is_primary' => true, 'primary_reason' => 'مخاطب اصلی', 'created_by' => $lead->assigned_to, ]); ContactPhone::create([ 'contact_id' => $contact->id, 'phone' => $phone, 'type' => 'mobile', 'status' => 'active', ]); return $contact; } }