Datos de llamada de VoIP

En el capítulo anterior tratamos el tema de los eventos que pueden ocurrir al finalizar una llamada.

  1. En este tutorial, consideramos que nuestra integración recibe un webhook del servicio VoIP cuando una llamada termina. Cuando se envía el webhook indicando el fin de la llamada, el backend de la integración debe buscar la entidad correspondiente (lead, contacto o compañía), o crear un Lead entrante si no existe ninguna entidad asociada al número telefónico en tu cuenta de Kommo.
  2. Si existe un contacto con ese número de teléfono, puedes añadir una Nota de llamada con toda la información necesaria y vincularla al contacto mediante la API.
  3. Si la llamada finalizó desde Kommo, puedes mostrar una ventana modal para que el gerente añada información adicional sobre el resultado de llamada. Cuando el gerente completa los datos y guarda el resultado, esa información se envía al backend de la integración VoIP, que crea la Nota de llamada en la tarjeta de la entidad.
📘

Como mencionamos anteriormente, estos eventos no son obligatorios y solo representan un ejemplo de una integración VoIP con Kommo.

Al recibir un webhook que indica el final de una llamada, debemos registrar toda la información relevante de la llamada. Sin embargo, para evitar crear registros duplicados, primero debemos verificar si la llamada ya existe. Si la llamada no se encuentra, usaremos la API de Registro de llamadas para crear un nuevo registro de llamada.

Para implementar el registro de llamadas, hemos creado una clase que obtiene todas las llamadas desde el servicio VoIP y las almacena en el repositorio de llamadas dentro de la base de datos de la integración.

public static function getByCallIdAndKommoAccountId(string $callId, int $kommoAccountId): VoipCalls {
    return VoipCalls::query() 
        ->where('call_id', '=', $callId)
        ->where('kommo_account_id', '=', $kommoAccountId)
        ->first();
}

Para cada llamada se crea una tarea — ya sea para guardar una nueva llamada (SaveCallEventTask) o para actualizar una existente (UpdateCallTask). A continuación se muestra el constructor de la tarea:

public function __construct(
    private int $kommoAccountId,
    private string $toPhone,
    private string $fromPhone,
    private string $callId,
    private CallType $direction,
    private int $status,
    private int $duration,
    private int $startedAt,
    private int $userId,
    private ?string $recording = null
);
public function handle(UpdateCallTask $task): void
{
    $voipCall = voipCalls::getByCallIdAndKommoAccountId(
        $task->getCallId(),
        $task->getKommoAccountId()
    );

    $call = Call::fromModel($voipCall);//Call entity

    if ($record = $voipCall->getRecording() ?? '') {
            $record = sprintf(
                '%s/voip/%s/get_call_record/%s',
                $this->appConfig->getBaseUrl(),
                $task->getKommoAccountId(),
                $call->getCallId()
            );
    }
    $call->setRecordLink($record);

    // Llama a nuestra API de cliente para crear un evento de llamada.
    $this->callService->updateCallEvent(
                $call,
                $voipCall->getResponsibleUserId() ?? self::BOT_USER_ID,
                $voipCall->getEntityId(),
                $voipCall->getEntityType(),
                $voipCall->getParentId()
            );	
    $voipCall->setDelivered(DeliveryStatus::COMPLETED);
    $voipCall->save();
    $task->setSuccess(true);
}

Aquí se definen dos casos de uso: uno para guardar una llamada nueva (SaveCallEventUseCase) y otro para actualizar una llamada existente (UpdateCallUseCase). Cada caso de uso recibe como entrada la tarea correspondiente y la envía a la cola adecuada para su procesamiento:

public function handle(SaveCallEventTask $task): void
{
    $voipCall = VoipCalls::getByCallIdAndkommoAccountId(
        $task->getCallId(),
        $task->getKommoAccountId()
    );
    $isNew = $voipCall === null;
    $voipCall = $isNew ? VoipCalls::create() : $voipCall;

    $responsibleUser = $voipCall->getResponsibleUserId() ?? task->$getUserId();
    $voipCall
        ->setDirection($task->getDirection())
        ->setCallId($task->getCallId())
        ->setkommoAccountId($task->getkommoAccountId())
        ->setToNumber($task->getToPhone())
        ->setFromNumber($task->getFromPhone())
        ->setDuration($task->getDuration())
        ->setStartedAt($task->getStartedAt())
        ->setResponsibleUserId($responsibleUser)
        ->setRecording($task->getRecording());
    $voipCall->getStatus() ?: $voipCall->setStatus($task->getStatus());
    $voipCall->save();
  
    // Intentaremos registrar la llamada con la información básica
    // llamando a AddCallWorker desde
    // el caso de uso de registro de llamadas AddCallUseCase 
    $data = [
        'call_id' => $task->getCallId(),
        'account_id' => $task->getkommoAccountId(),
    ];
    $queueName = $isNew ? AddCallWorker::QUEUE_NAME : UpdateCallNoteWorker::QUEUE_NAME;
    $queueTask = new QueueTask($queueName, $data);
    $this->queue->send($queueTask);
    $task->setSuccess(true);
}
public function handle(UpdateCallTask $task): void
{
    $voipCall = voipCalls::getByCallIdAndKommoAccountId(
        $task->getCallId(),
        $task->getKommoAccountId()
    );

    $call = Call::fromModel($voipCall); // Entidad de llamada

    if ($record = $voipCall->getRecording() ?? '') {
            $record = sprintf(
                '%s/voip/%s/get_call_record/%s',
                $this->appConfig->getBaseUrl(),
                $task->getKommoAccountId(),
                $call->getCallId()
            );
    }
    $call->setRecordLink($record);

    // Llama a nuestra API de cliente para crear un evento de llamada.
    $this->callService->updateCallEvent(
                $call,
                $voipCall->getResponsibleUserId() ?? self::BOT_USER_ID,
                $voipCall->getEntityId(),
                $voipCall->getEntityType(),
                $voipCall->getParentId()
            );	
    $voipCall->setDelivered(DeliveryStatus::COMPLETED);
    $voipCall->save();
    $task->setSuccess(true);
}

Al guardar el evento de llamada, o bien creamos una nueva llamada utilizando el caso ProcessCallWebhookWorker, o actualizamos una existente con UpdateCallNoteWorker. Definamos los dos workers:

public function run(array $data, LoggerInterface $logger): void
{
    $taskData = $data['data'];
    $webhookData = FromWebhook::fromArray($data['data']['webhook_data']);
    $call = [
        'call_id' => $webhookData->getCallId() ?? (string)$webhookData->getSessionId(),
        'to_number' => $webhookData->getToPhoneNumber(),
        'from_number' => $webhookData->getFromPhoneNumber(),
        'direction' => $webhookData->getDirection(),
        'duration' => 0,
        'call_result' => 'done',
        'recording' => null,
        'started_at' => $webhookData->getStartTime()->toIso8601ZuluString()
    ];
    $call = Call::fromArray($call);
    $user = VoipUsers::getByExtensionId($webhookData->getExtension());
    $call->setResponsibleUser($user->getUserId())
    $task = new SaveCallEventTask( 
        (int)$taskData['kommo_account_id'], 
        $call->getToNumber(), 
        $call->getFromNumber(), 
        $call->getCallId(), 
        $call->getCallType(), 
        $call->getDirection(), 
        $call->getDuration(), 
        $call->getStartedAt(), 
        $call->getResponsibleUser(), 
        null 
    ); 
    $this->saveCallEventUseCase->handle($task); 
    if (!$task->isSuccess()) 
    { 
        throw BusinessLogicException::create('Guardar error de evento de llamada'); 
    } 
}
public function run(array $data, LoggerInterface $logger): void 
{ 
    $taskData = $data['data']; 
    $task = new UpdateCallTask($taskData['account_id'], $taskData['call_id']); 
    $this->updateCallUseCase->handle($task); 
    if (!$task->isSuccess())
    { 
        throw BusinessLogicException::create('Error en la llamada de actualización'); 
    } 
} 

Además, necesitamos manejar el webhook entrante y enviar la tarea correspondiente a la cola:

public function handle(ServerRequestInterface $request): ResponseInterface
{
    $data = $request->getParsedBody();
    if ($data) {
        $callWebhook = FromWebhook::fromArray($data);
        $queueTask = new QueueTask(
            ProcessCallWebhookWorker::QUEUE_NAME,
            [
                'kommo_account_id' => (int)$data['kommo_account_id'],
                'webhook_data' => $callWebhook->toArray(),
            ]
        );
        $this->queue->send($queueTask);
    }
}