CRM/backend/app/Services/CallService.php

311 خطوط
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\Call;
use App\Models\CallLog;
use App\Models\CallResult;
use App\Models\Contact;
use App\Models\ContactPhone;
use App\Models\ContactRelation;
use App\Models\Lead;
use App\Models\LeadStatus;
use App\Models\FollowUp;
use App\Models\PipelineStage;
use App\Models\Setting;
use App\Models\User;
use App\Services\ActivityLogger;
use App\Services\VoIP\VoIPManager;
use App\Support\WorkingHours;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class CallService
{
public function __construct(private VoIPManager $voipManager) {}
public function initiateCall(int $leadId, int $userId, ?int $contactPhoneId = null, bool $includePhone = true): array
{
$lead = Lead::with('contacts.phones')->findOrFail($leadId);
$user = User::findOrFail($userId);
$callerExtension = trim((string) ($user->voip_extension ?: ''));
$providerName = Setting::where('key', 'voip_provider')->value('value') ?: 'mock';
if ($providerName === 'ami' && $callerExtension === '') {
throw ValidationException::withMessages([
'voip_extension' => ['شماره داخلی برای این کاربر ثبت نشده است.'],
]);
}
if (!$lead->assigned_to) {
$lead->update([
'assigned_to' => $userId,
'assigned_by' => $userId,
'is_unassigned' => false,
'pipeline_stage_id' => PipelineStage::where('slug', 'waiting_call')->value('id') ?? $lead->pipeline_stage_id,
]);
\App\Models\LeadAssignment::create([
'lead_id' => $lead->id,
'user_id' => $userId,
'assigned_by' => $userId,
'method' => 'call_claim',
]);
}
$phone = $this->resolvePhone($lead, $contactPhoneId);
$callerId = $callerExtension !== '' ? $callerExtension : (string) $userId;
$providerResult = $this->voipManager->initiateCall($phone->phone, $callerId);
if (($providerResult['success'] ?? false) !== true) {
throw ValidationException::withMessages([
'voip' => [$providerResult['message'] ?? 'درخواست تماس به مرکز تلفن ارسال نشد.'],
]);
}
$providerCallId = $providerResult['provider_call_id'] ?? null;
$recordingEnabled = Setting::where('key', 'call_recording_enabled')->value('value') === 'true';
$call = Call::create([
'lead_id' => $leadId,
'contact_id' => $phone->contact_id,
'contact_phone_id' => $phone->id,
'user_id' => $userId,
'direction' => 'outbound',
'phone' => $phone->phone,
'result' => null,
'provider_call_id' => $providerCallId,
'recording_url' => $recordingEnabled && $providerCallId ? $this->voipManager->getRecordingUrl($providerCallId) : null,
'is_manual' => true,
]);
$phone->increment('call_count');
$phone->update([
'last_called_at' => now(),
'last_called_by' => $userId,
]);
CallLog::create([
'lead_id' => $leadId,
'contact_id' => $phone->contact_id,
'contact_phone_id' => $phone->id,
'call_id' => $call->id,
'user_id' => $userId,
'called_at' => now(),
'recording_url' => $call->recording_url,
]);
$lead->update([
'last_call_at' => now(),
'call_attempts' => $lead->call_attempts + 1,
]);
ActivityLogger::log('call_initiated', "Call initiated for lead {$leadId}", $call);
$payload = [
'call_id' => $call->id,
'contact_id' => $phone->contact_id,
'contact_phone_id' => $phone->id,
'lead_name' => $lead->full_name,
'recording_enabled' => $recordingEnabled,
'recording_url' => $call->recording_url,
'provider_call_id' => $providerCallId,
];
if ($includePhone) {
$payload['phone'] = $phone->phone;
}
return $payload;
}
public function registerResult(int $callId, string $result, ?string $notes = null, ?string $followUpAt = null, ?array $referral = null, array $options = []): Call
{
$call = Call::findOrFail($callId);
return DB::transaction(function () use ($call, $callId, $result, $notes, $followUpAt, $referral, $options) {
$call->update([
'result' => $result,
'notes' => $notes,
]);
$lead = $call->lead;
$lead->update([
'last_call_result' => $result,
'last_call_at' => now(),
]);
$callResult = CallResult::where('name', $result)->orWhere('slug', $result)->first();
$nextAction = $callResult?->next_action ?? ($followUpAt ? 'پیگیری در زمان مشخص‌شده' : null);
if ($callResult?->next_pipeline_stage_id) {
$lead->update(['pipeline_stage_id' => $callResult->next_pipeline_stage_id]);
} elseif ($followUpAt && $followUpStageId = PipelineStage::where('slug', 'follow_up')->value('id')) {
$lead->update(['pipeline_stage_id' => $followUpStageId]);
}
if ($callResult?->lead_status_id) {
$lead->update(['lead_status_id' => $callResult->lead_status_id]);
}
if ($call->contactPhone) {
$success = (bool) $callResult?->is_positive;
$call->contactPhone->increment($success ? 'successful_call_count' : 'failed_call_count');
$call->contactPhone->update([
'last_call_result' => $result,
'last_called_at' => now(),
'last_called_by' => $call->user_id,
'status' => $callResult?->phone_status ?? $call->contactPhone->status,
]);
}
$newContact = null;
if (in_array($result, ['معرفی شماره یا شخص جدید', 'معرفی شماره جدید'], true) && $referral) {
$newContact = $this->createReferralContact($lead, $call, $referral, $notes);
$nextAction = 'تماس با مخاطب معرفی‌شده';
$followUpAt = $referral['next_follow_up_at'] ?? $followUpAt;
}
if ($result === 'شماره اشتباه' && ($options['mark_phone_wrong'] ?? false) && $call->contactPhone) {
$call->contactPhone->update(['status' => 'wrong']);
ActivityLogger::log('contact_phone_marked_wrong', "Contact phone {$call->contactPhone->id} marked wrong from mobile call result", $call->contactPhone);
}
if ($result === 'عدم تمایل') {
$reason = $options['disinterest_reason'] ?? null;
if (($options['close_lead'] ?? false) === true) {
$lead->update([
'final_result' => 'ناموفق',
'lost_reason' => $reason,
'lead_status_id' => LeadStatus::where('slug', 'lost')->value('id') ?? $lead->lead_status_id,
'pipeline_stage_id' => PipelineStage::where('slug', 'closed')->value('id') ?? $lead->pipeline_stage_id,
'next_follow_up_at' => null,
]);
$nextAction = 'لید بسته شد';
ActivityLogger::log('lead_closed_from_mobile_call_result', "Lead {$lead->id} closed after disinterest: {$reason}", $lead);
} elseif ($call->contact) {
$call->contact->update(['status' => 'inactive']);
$nextAction = 'مخاطب غیرفعال شد';
ActivityLogger::log('contact_marked_inactive_from_mobile_call_result', "Contact {$call->contact->id} marked inactive after disinterest: {$reason}", $call->contact);
}
}
if ($followUpAt) {
if (!WorkingHours::followUpAllowed($followUpAt)) {
throw \Illuminate\Validation\ValidationException::withMessages([
'next_follow_up_at' => ['زمان پیگیری خارج از روز یا ساعت کاری مجاز است.'],
]);
}
$lead->update(['next_follow_up_at' => $followUpAt]);
FollowUp::create([
'lead_id' => $lead->id,
'user_id' => $call->user_id,
'call_id' => $call->id,
'scheduled_at' => $followUpAt,
'status' => 'pending',
'notes' => $newContact ? "پیگیری مخاطب معرفی‌شده: {$newContact->name}" : $notes,
]);
NotificationService::notifyFollowUpReminder($call->user_id, $lead->full_name ?: ($lead->company ?? "لید {$lead->id}"));
}
CallLog::updateOrCreate(
['call_id' => $call->id],
[
'lead_id' => $lead->id,
'contact_id' => $call->contact_id,
'contact_phone_id' => $call->contact_phone_id,
'user_id' => $call->user_id,
'called_at' => $call->created_at ?? now(),
'result' => $result,
'notes' => $notes,
'next_action' => $nextAction,
'recording_url' => $call->recording_url,
]
);
ActivityLogger::log('call_result_registered', "Call {$callId} result: {$result}", $call);
return $call->fresh(['lead', 'contact', 'contactPhone']);
});
}
private function resolvePhone(Lead $lead, ?int $contactPhoneId): ContactPhone
{
if ($contactPhoneId) {
return ContactPhone::whereHas('contact', fn($query) => $query->where('lead_id', $lead->id))
->findOrFail($contactPhoneId);
}
$contact = $lead->contacts->firstWhere('is_primary', true) ?? $lead->contacts->first();
if ($contact) {
$phone = $contact->phones->firstWhere('status', 'active') ?? $contact->phones->first();
if ($phone) {
return $phone;
}
}
$contact = Contact::create([
'lead_id' => $lead->id,
'name' => $lead->full_name ?: ($lead->company ?? 'مخاطب اولیه'),
'role' => 'رابط',
'status' => 'active',
'is_primary' => true,
'primary_reason' => 'مخاطب اولیه لید',
'created_by' => $lead->assigned_to,
]);
return ContactPhone::create([
'contact_id' => $contact->id,
'phone' => $lead->phone,
'type' => 'mobile',
'status' => 'active',
]);
}
private function createReferralContact(Lead $lead, Call $call, array $referral, ?string $notes): Contact
{
$makePrimary = (bool) ($referral['make_primary'] ?? false);
if ($makePrimary) {
Contact::where('lead_id', $lead->id)->update(['is_primary' => false]);
}
$fromContact = $call->contact;
$primaryReason = $fromContact
? "معرفی‌شده توسط {$fromContact->name}"
: 'معرفی‌شده در تماس';
$contact = Contact::create([
'lead_id' => $lead->id,
'name' => $referral['name'],
'role' => $referral['role'] ?? null,
'description' => $referral['description'] ?? $notes,
'status' => 'active',
'is_primary' => $makePrimary,
'primary_reason' => $makePrimary ? $primaryReason : null,
'created_by' => $call->user_id,
]);
ContactPhone::create([
'contact_id' => $contact->id,
'phone' => $referral['phone'],
'type' => $referral['phone_type'] ?? 'mobile',
'status' => 'active',
]);
ContactRelation::create([
'lead_id' => $lead->id,
'from_contact_id' => $call->contact_id,
'to_contact_id' => $contact->id,
'relation_type' => 'introduced',
'description' => $referral['relation_description'] ?? null,
'created_by' => $call->user_id,
]);
if ($makePrimary) {
ActivityLogger::log('primary_contact_changed', "Primary contact changed to {$contact->name}: {$primaryReason}", $lead);
}
return $contact;
}
}