252 lines
8.3 KiB
PHP
252 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\Crm\Services;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Crm\Entities\CrmCallQueue;
|
|
use Modules\Crm\Entities\CrmCallSession;
|
|
use Modules\Crm\Entities\CrmPhoneFollowUp;
|
|
use Modules\Crm\Utils\TelemarketingUtil;
|
|
|
|
class CallQueueService
|
|
{
|
|
public function syncQueueForBusiness(int $business_id): void
|
|
{
|
|
$this->pruneInvalidQueueEntries($business_id);
|
|
|
|
$phones = CrmPhoneFollowUp::where('business_id', $business_id)
|
|
->where('status', 'pending')
|
|
->where('block_auto_recall', false)
|
|
->get();
|
|
|
|
foreach ($phones as $phone) {
|
|
$exists = CrmCallQueue::where('business_id', $business_id)
|
|
->where('phone_follow_up_id', $phone->id)
|
|
->exists();
|
|
|
|
if (! $exists) {
|
|
CrmCallQueue::create([
|
|
'business_id' => $business_id,
|
|
'phone_follow_up_id' => $phone->id,
|
|
'campaign_id' => $phone->campaign_id,
|
|
'status' => 'available',
|
|
'priority' => $phone->priority ?? 3,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove queue slots for numbers already called or no longer pending.
|
|
*/
|
|
public function pruneInvalidQueueEntries(int $business_id): void
|
|
{
|
|
$now = Carbon::now();
|
|
|
|
CrmCallQueue::where('business_id', $business_id)
|
|
->whereIn('status', ['available', 'locked'])
|
|
->whereHas('phoneFollowUp', function ($q) {
|
|
$q->where('status', '!=', 'pending');
|
|
})
|
|
->update([
|
|
'status' => 'skipped',
|
|
'completed_at' => $now,
|
|
'locked_by' => null,
|
|
'locked_at' => null,
|
|
'expires_at' => null,
|
|
]);
|
|
|
|
$donePhoneIds = CrmCallQueue::where('business_id', $business_id)
|
|
->whereIn('status', ['completed', 'skipped'])
|
|
->pluck('phone_follow_up_id')
|
|
->unique();
|
|
|
|
if ($donePhoneIds->isNotEmpty()) {
|
|
CrmCallQueue::where('business_id', $business_id)
|
|
->whereIn('status', ['available', 'locked'])
|
|
->whereIn('phone_follow_up_id', $donePhoneIds)
|
|
->update([
|
|
'status' => 'skipped',
|
|
'completed_at' => $now,
|
|
'locked_by' => null,
|
|
'locked_at' => null,
|
|
'expires_at' => null,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function acquireNext(int $business_id, int $user_id, ?int $campaign_id = null): ?CrmCallQueue
|
|
{
|
|
return DB::transaction(function () use ($business_id, $user_id, $campaign_id) {
|
|
$this->syncQueueForBusiness($business_id);
|
|
|
|
$query = CrmCallQueue::where('crm_call_queues.business_id', $business_id)
|
|
->where('crm_call_queues.status', 'available')
|
|
->join('crm_phone_follow_ups', 'crm_call_queues.phone_follow_up_id', '=', 'crm_phone_follow_ups.id')
|
|
->where('crm_phone_follow_ups.status', 'pending')
|
|
->where('crm_phone_follow_ups.block_auto_recall', false)
|
|
->select('crm_call_queues.*')
|
|
->orderBy('crm_call_queues.priority')
|
|
->orderBy('crm_call_queues.id');
|
|
|
|
if (! auth()->user()->can('crm.telemarketing.view_all_queue') && ! auth()->user()->can('superadmin')) {
|
|
$query->where(function ($q) use ($user_id) {
|
|
$q->whereNull('crm_phone_follow_ups.assigned_to')
|
|
->orWhere('crm_phone_follow_ups.assigned_to', $user_id);
|
|
});
|
|
}
|
|
|
|
if (! empty($campaign_id)) {
|
|
$query->where('crm_call_queues.campaign_id', $campaign_id);
|
|
}
|
|
|
|
$queue = $query->lockForUpdate()->first();
|
|
|
|
if (empty($queue)) {
|
|
return null;
|
|
}
|
|
|
|
$lockMinutes = TelemarketingUtil::callLockMinutes($business_id);
|
|
$now = Carbon::now();
|
|
|
|
$queue->update([
|
|
'status' => 'locked',
|
|
'locked_by' => $user_id,
|
|
'locked_at' => $now,
|
|
'expires_at' => $now->copy()->addMinutes($lockMinutes),
|
|
]);
|
|
|
|
return $queue->fresh(['phoneFollowUp']);
|
|
});
|
|
}
|
|
|
|
public function release(int $queue_id, int $user_id, ?int $business_id = null): bool
|
|
{
|
|
$query = CrmCallQueue::query()->where('id', $queue_id);
|
|
if ($business_id) {
|
|
$query->where('business_id', $business_id);
|
|
}
|
|
$queue = $query->first();
|
|
if (empty($queue) || $queue->locked_by != $user_id) {
|
|
return false;
|
|
}
|
|
|
|
if ($queue->status === 'in_call') {
|
|
return false;
|
|
}
|
|
|
|
$queue->update([
|
|
'status' => 'available',
|
|
'locked_by' => null,
|
|
'locked_at' => null,
|
|
'expires_at' => null,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function startSession(int $queue_id, int $user_id, ?int $business_id = null): ?CrmCallSession
|
|
{
|
|
return DB::transaction(function () use ($queue_id, $user_id, $business_id) {
|
|
$query = CrmCallQueue::query()->where('id', $queue_id);
|
|
if ($business_id) {
|
|
$query->where('business_id', $business_id);
|
|
}
|
|
$queue = $query->lockForUpdate()->first();
|
|
|
|
if (empty($queue) || $queue->locked_by != $user_id) {
|
|
return null;
|
|
}
|
|
|
|
$session = CrmCallSession::create([
|
|
'business_id' => $queue->business_id,
|
|
'queue_id' => $queue->id,
|
|
'phone_follow_up_id' => $queue->phone_follow_up_id,
|
|
'user_id' => $user_id,
|
|
'started_at' => Carbon::now(),
|
|
]);
|
|
|
|
$queue->update([
|
|
'status' => 'in_call',
|
|
'call_session_id' => $session->id,
|
|
]);
|
|
|
|
return $session->load('phoneFollowUp');
|
|
});
|
|
}
|
|
|
|
public function endSession(int $session_id, int $user_id, ?string $outcome = null, ?int $business_id = null): ?CrmCallSession
|
|
{
|
|
return DB::transaction(function () use ($session_id, $user_id, $outcome, $business_id) {
|
|
$query = CrmCallSession::query()->where('id', $session_id);
|
|
if ($business_id) {
|
|
$query->where('business_id', $business_id);
|
|
}
|
|
$session = $query->first();
|
|
|
|
if (empty($session) || $session->user_id != $user_id || ! empty($session->ended_at)) {
|
|
return null;
|
|
}
|
|
|
|
$session->update([
|
|
'ended_at' => Carbon::now(),
|
|
'outcome' => $outcome,
|
|
]);
|
|
|
|
if (! empty($session->queue_id)) {
|
|
CrmCallQueue::where('id', $session->queue_id)->update([
|
|
'status' => 'completed',
|
|
'completed_at' => Carbon::now(),
|
|
]);
|
|
}
|
|
|
|
return $session->fresh();
|
|
});
|
|
}
|
|
|
|
public function getActiveSession(int $business_id, int $user_id): ?CrmCallSession
|
|
{
|
|
return CrmCallSession::where('business_id', $business_id)
|
|
->where('user_id', $user_id)
|
|
->whereNull('ended_at')
|
|
->with(['phoneFollowUp', 'queue'])
|
|
->latest('started_at')
|
|
->first();
|
|
}
|
|
|
|
public function getActiveQueue(int $business_id, int $user_id): ?CrmCallQueue
|
|
{
|
|
return CrmCallQueue::where('business_id', $business_id)
|
|
->where('locked_by', $user_id)
|
|
->whereIn('status', ['locked', 'in_call'])
|
|
->with('phoneFollowUp')
|
|
->latest('locked_at')
|
|
->first();
|
|
}
|
|
|
|
public function expireStaleLocks(): int
|
|
{
|
|
$expired = CrmCallQueue::whereIn('status', ['locked', 'in_call'])
|
|
->whereNotNull('expires_at')
|
|
->where('expires_at', '<', Carbon::now())
|
|
->get();
|
|
|
|
$count = 0;
|
|
foreach ($expired as $queue) {
|
|
if ($queue->status === 'in_call') {
|
|
continue;
|
|
}
|
|
$queue->update([
|
|
'status' => 'available',
|
|
'locked_by' => null,
|
|
'locked_at' => null,
|
|
'expires_at' => null,
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
}
|