311 خطوط
12 KiB
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;
|
|
}
|
|
|
|
}
|