Делаем очередь входящих звонков с функцией callback

  • Tutorial
При звонках в колл-центр компаний часто приходится сталкиваться с большим временем ожидания на линии, многие из нас слушали надоедливую мелодию в течение десятков минут хотя бы раз в жизни. Самое интересное заключается в том, что это совершенно невыгодно компании в колл-центр которой вы звоните, особенно если вы звоните на номер 8-800. К счастью, уже давно придуман способ, позволяющий решить данную проблему — это callback или обратный вызов. Суть этого способа очень простая: позвонившему предлагают отключиться от колл-центра при этом его номер так и остается в очереди на обслуживание и как только его очередь подойдет, то ему автоматически наберут и соединят с оператором, на которого распределился его звонок. Таким образом убиваем сразу несколько зайцев: не занимаются линии колл-центра, не тратятся дорогостоящие минуты 8-800, а клиенты не испытывают лишнего раздражения в ожидании ответа. Вендоры колл-центрового ПО хотят за такую функцию весьма приличных денег, а мы под катом расскажем как данный функционал достаточно просто и быстро реализуется с помощью платформы VoxImplant.
Так как мы уже писали про организацию колл-центра с помощью VoxImplant, то повторять эту часть в данной статье не будем, а сосредоточимся на изменениях, которые необходимо сделать в сценариях для реализации коллбэк-функции. Сразу замечу, что данная функциональность доступна только для сценариев, в которых подключается модуль ACD для работы с очередями. Сценарий, отвечающий за обработку входящих звонков с учетом нашей коллбэк-функции будет выглядеть следующим образом:
// Подключаем модуль ACD
require(Modules.ACD);

var request, // <-- тут будем хранить экземпляр ACDRequest
	originalCall, // <-- входящий звонок
	callerid,
	statusInterval,
	callback = false; // <-- флаг для коллбэк-режима

// Вешаем обработчик входящего вызова
VoxEngine.addEventListener(AppEvents.CallAlerting, handleInboundCall);

// Обрабатываем входящий вызов
function handleInboundCall(e) {
	originalCall = e.call;
	callerid = e.callerid;
	// Вешаем обработчики
	originalCall.addEventListener(CallEvents.Connected, handleCallConnected);
	originalCall.addEventListener(CallEvents.PlaybackFinished, handlePlaybackFinished);
	originalCall.addEventListener(CallEvents.Failed, cleanup);
	originalCall.addEventListener(CallEvents.Disconnected, cleanup);
	// Отвечаем на звонок
	originalCall.answer();
}

// Завершаем сессию
function cleanup(e) {
	if (request) {
		// Если звонок в очереди - удаляем
		request.cancel();
		request = null;
	}
	// Закончить сессию
	if (!callback) VoxEngine.terminate();
}

// Играем музыку после окончания проигрывания голоса или музыки
function handlePlaybackFinished(e) {
	e.call.startPlayback("http://cdn.voximplant.com/toto.mp3");
}

// Обработчик нажатий на кнопки
function handleToneReceived(e) {
	if (e.tone == "#") {
		callback = true;
		originalCall.hangup(); // <--  несмотря на отсутствие звонков сессия не завершится
	}
}

// Звонок соединен
function handleCallConnected(e) {
	// Включаем обработку нажатий на кнопки телефона
	originalCall.handleTones(true);
	originalCall.addEventListener(CallEvents.ToneReceived, handleToneReceived);
	// Отправляем звонок в очередь 'MainQueue', которую мы создали в панели управления
	request = VoxEngine.enqueueACDRequest("MainQueue", callerid);

	// Получаем статус после того как звонок поставлен в очередь
	request.addEventListener(ACDEvents.Queued, function (acdevent) {
		request.getStatus();
	});

	// Сообщаем звонящему о примерном времени ожидания и месте в очереди
	request.addEventListener(ACDEvents.Waiting, function (acdevent) {
		var minutesLeft = acdevent.ewt + 1;
		var minutesWord = " минуты.";
		if ((minutesLeft > 10 && minutesLeft < 20) || (minutesLeft % 10 > 4 || minutesLeft % 10 == 0)) {
			minutesWord = " минут.";
		} else if (minutesLeft % 10 == 1) {
			minutesWord = " минуту.";
		}
		originalCall.say("Вы находитесь в очереди под номером " + acdevent.position +
			". Оператор ответит Вам менее чем через " + (acdevent.ewt + 1) + minutesWord +
			" Вы также можете нажать решетку и мы сами перезвоним вам как только оператор будет готов обслужить ваш вызов.", Language.RU_RUSSIAN_FEMALE);
	});

	// Отправляем звонок оператору
	request.addEventListener(ACDEvents.OperatorReached, function (acdevent) {
		if (callback) { 
                // В случае если клиент выбрал коллбэк, то после распределения вызова на оператора сообщаем оператору, что нужно дождаться соединения с клиентом
			acdevent.operatorCall.say("Пожалуйста, дождитесь соединения с клиентом.", Language.RU_RUSSIAN_FEMALE);
			acdevent.operatorCall.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);
			acdevent.operatorCall.addEventListener(CallEvents.PlaybackFinished, function (callevent) {
                // Делаем коллбэк. В качестве caller id указываем номер, подтвержденный через верхнее меню панели управления Voximplant.
				const callerId = "+1234567890"
				originalCall = VoxEngine.callPSTN(callerid, callerId); 
				originalCall.addEventListener(CallEvents.Connected, function (callevent) {
					VoxEngine.sendMediaBetween(acdevent.operatorCall, originalCall);
					clearInterval(statusInterval);
				});
				originalCall.addEventListener(CallEvents.Failed, cleanup);
				originalCall.addEventListener(CallEvents.Disconnected, cleanup);
			});
		} else {
			VoxEngine.sendMediaBetween(acdevent.operatorCall, originalCall);
			acdevent.operatorCall.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);
			clearInterval(statusInterval);
		}
	});   

	// Нет доступных операторов - ни один оператор не обслуживает очередь
	request.addEventListener(ACDEvents.Offline, function (acdevent) {
		originalCall.say("К сожалению, сейчас нет доступных операторов. Пожалуйста, попробуйте позвонить позднее.", Language.RU_RUSSIAN_FEMALE);
		originalCall.addEventListener(CallEvents.PlaybackFinished, function (e) {
			VoxEngine.terminate();
		});
	});

	// Получаем и сообщаем статус каждые 30 секунд
	statusInterval = setInterval(request.getStatus, 30000);
}

Вот собственно и все. Немного подправив сценарий из предыдущего туториала про ACD мы получили очередь с коллбэком. Базовый сценарий, который мы меняли, доступен на github.com/voximplant/acd, также как и простейший веб-фон для операторов.

Работа с очередью через HTTP API


Предыдущий пример был сделан для случая когда сначала есть входящий звонок, в реальной жизни могут встречаться случаи, когда клиент сразу заказывает на сайте коллбэк и нам нужно его разместить в очереди на обработку. В целом, очередь — это абстрактная сущность, туда на обработку можно кидать не только звонки, но и, например, emailы, сообщения и т.д. В такой ситуации сценарий надо запускать через метод StartScenarios HTTP API, а сам сценарий немного трансформируется:
var displayName, callback = true;
// Обработка заупуска сессии через StartScenarios
VoxEngine.addEventListener(AppEvents.Started, function(e) {
	var data = VoxEngine.customData();
    // Будем в script_custom_data передавать имя клиента и его номер в виде номер:имя
    data = data.split(":");
	callerid = data[0];
  	displayName = data[1];
  	Logger.write("Put "+displayName+" with number "+callerid+" in a queue");
  	// Ставим запрос в очередь 'MainQueue'
	request = VoxEngine.enqueueACDRequest("MainQueue", callerid);
     // ... дальше все то же самое, что и в предыдущем сценарии
});


P.S. А что если...


Нас иногда спрашивают: «а что если мы хотим использовать SIP-телефоны вместо веб-телефонов, сделанных с помощью VoxImplant Web SDK в нашем колл-центре?»
Отвечаем: «как ни странно, это возможно, но подробнее об этом мы расскажем в отдельной статье».

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Понравился ли вам данный материал?

Voximplant
140,23
Облачная платформа голосовой и видеотелефонии
Поделиться публикацией

Комментарии 19

    0
    А как сделать так, чтобы callback ввели во всех техподдержках? Пожааалуйста…
      0
      Порекомендовать им ознакомиться с нашей статьей, как вариант. Ну или обратиться к правительству и законодательно обязать :)
        0
        Вы сообщаете клиенту о примерном периоде времени, в течении которого ему перезвонит оператор?
          +1
          Да, в коде есть часть где сообщается время ожидания в очереди, оно, по сути, равно времени через которое ему перезвонят, если он решил воспользоваться коллбэком. Перед тем как его отключить можно ему сообщить. Будет что-то в духе:
          // Обработчик нажатий на кнопки
          function handleToneReceived(e) {
              if (e.tone == "#") {
                  callback = true;
                  originalCall.removeEventListener(CallEvents.PlaybackFinished, handlePlaybackFinished);
                  originalCall.say("Оператор перезвонит Вам менее чем через " + (ewt + 1) + minutesWord, Language.RU_RUSSIAN_FEMALE);
                  originalCall.addEventListener(CallEvents.PlaybackFinished, function(callevent) {
                       originalCall.hangup(); // <--  несмотря на отсутствие звонков сессия не завершится
                  });
              }
          }
          

          Данные ewt и minutesWord можно взять из обработчика ACDEvents.Waiting
            0
            А выбор есть у клиента, ждать ответа или ждать обратного звонка? Или Вы это решаете за него?
              0
              Если внимательно посмотреть на код, то можно увидеть ответ на ваш вопрос:
              "Вы находитесь в очереди под номером " + acdevent.position +
                          ". Оператор ответит Вам менее чем через " + (acdevent.ewt + 1) + minutesWord +
                          " Вы также можете нажать решетку и мы сами перезвоним вам как только оператор будет готов обслужить ваш вызов."
              
                –4
                Зачем мне внимательно смотреть ваш код? У нас используется совсем другая платформа. Мне интересны именно ключевые моменты самой идеи, а не конкретная реализация на какой-то там платформе. Трудно было просто ответить на вопрос?
                  0
                  По-моему я вполне ответил на ваш вопрос. Статья про конкретную реализацию.
                    –2
                    Ответили, только при этом не упустили шанса упрекнуть человека в его невнимательности.
      +1
      // Делаем коллбэк
      originalCall = VoxEngine.callUser(callerid);
      Опечатка с callUser/callPSTN?

      Непонятно, почему сессия не завершается — как сценарий определяет (по подключению модуля ACD / наличию переменной callback)? И как её тогда завершить, если функционал callback'a не нужен, а модуль очередей нужен?

      Как долго висит выполнение сценария, если все операторы заняты — часы, дни, бесконечно? Как удалить callback-звонок из очереди?
        0
        Да, совершенно верно, опечатка :) Спасибо, что заметили, сейчас поправим. Сессия не будет завершаться, если был использован модуль ACD и добавление в очередь, в таком состоянии сессия будет жить 2 часа, после чего самоуничтожится, так как мы исходим из того, что очередь все-таки быстрее обрабатывается. Callback-звонок из очереди можно удалить, получив media_session_access_url и сделав по нему запрос, в обработчике которого вызвать функцию cleanup
          0
          Еще вопрос:
          // Отправляем звонок оператору
          request.addEventListener(ACDEvents.OperatorReached, function (acdevent) {
          if (callback)
          Если callback=false (звонивший не нажимал решетку), то что происходит таком случае в else? Входящего звонка же уже нет (звонивший положил трубку), то есть соединять не с кем — просто корректно завершается сессия?

          Вообще, очень хочется, чтобы функция callback'a была включаема по-желанию, а не по-умолчанию. Чтобы очередь не засорялась звонящими, которые положили трубку и не захотели callback'a. Ведь сейчас, как я понял, даже если они не выбрали callback, они всё равно попадают в очередь и висят там два часа, что влияет на количество абонентов и время ожидания, которое сообщается новым звонящим.
            0
            Если callback = false, то человек будет дальше висеть и слушать музыку и обновления на тему сколько ему еще ждать осталось. Этот пример как раз про включение по желанию при нажатии клиентом #, если он не хочет чтобы ему перезвонили, то пусть слушает дальше музыку пока ему не ответят, а если он сам положит трубку, то сессию надо просто убить VoxEngine.terminate и все.
              0
              По коду в примере, при нажатие звонящим решетки срабатывает hangup, который вызывает disconnect и выполнение cleanup, в котором выполняется VoxEngine.terminate. Если же звонящий просто кладет трубку, то выполняется тот же самый disconnect->cleanup->terminate. Почему же в одном случае сессия не убивается, а в другом убивается? Или hangup не вызывает событие disconnect?

              По поводу callback=false, в том куске который я привел для примера, при выполнение else будет выполнен
              VoxEngine.sendMediaBetween(acdevent.operatorCall, originalCall);
              но originalCall уже не существует, так как абонент повесил трубку, но в очереди еще висит, и сценарий висит, поэтому я и спрашиваю что получится — корректно ли завершится всё?
                0
                Да, есть небольшая недоработка. Поправил функцию cleanup
                if (!callback) VoxEngine.terminate();
                


                Можно еще, как вариант, в случае если callback, то сделать
                originalCall.removeEventListener(CallEvents.Disconnected, cleanup);
                
                  0
                  Получается terminate полностью завершит сценарий и из очереди абонент исчезнет, если он не выбрал callback и положил трубку?
                    0
                    Да, сессия завершится после terminate
                      0
                      Теперь всё ясно, спасибо :)
                        0
                        Это Вам спасибо, что нашли несколько весьма досадных ошибок в сценарии :)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое