Call data from VoIP

In the previous chapter we have covered the topic about events that can occur by the end of a call:

  1. In this tutorial we consider our integration to receive a webhook from a VoIP service when a call ends. A webhook about ending the call is launched from the VoIP service. Then the integration backend needs to find an appropriate entity (lead, contact or company), or create an incoming lead if no entity connected to the phone number exists in your Kommo account.
  2. If there is a contact with the phone number, you can add call note with all calling data you need and link it with the contact via API.
  3. If the call was ended from Kommo, you can render a modal window to add extra data on call result from a manager. If the manager fills in the call data and saves the result, the data is passed to the VoIP integration backend and it creates call note in the entity card. If the call note has been already created from webhook, you can edit the note and add the new data from modal window to it
📘

As we previously mentioned these events aren’t mandatory and display an example of Kommo VoIP integration.

Upon receiving a webhook indicating the end of a call, we need to log all relevant call information. However, to avoid creating duplicate records, we should first check whether the call already exists. If the call is not found, we will use the Call log API to create a new call record. If it already exists, we will simply update the associated call note by Editing note request.

For implementing call logging we've created a class to grab all calls from the VoIP service and store them in the calls repository in the integration database.

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

For each call, a task is created — either to save a new call (SaveCallEventTask) or to update an existing one (UpdateCallTask). The task constructor is shown below:

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 __construct(
    private int $kommoAccountId,
    private string $callId,
);

Here two use cases are defined: one for saving a new call (SaveCallEventUseCase) and another for updating an existing call (UpdateCallUseCase). Each use case takes the appropriate task as input and sends it to the corresponding queue for processing:

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();
  
    // We will try to log the call with basic information 
    // by calling the AddCallWorker from 
    // the call logging use case 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);//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);

    // Call our client API to create call event
    $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);
}

When saving the call event, we either create a new call using the ProcessCallWebhookWorker case or update an existing one by UpdateCallNoteWorker. Let’s define the two 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('Save call event error'); 
    } 
} 
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('Update call error'); 
    } 
} 

Additionally, we need to handle the incoming webhook and submit the appropriate task to the queue.

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);
    }
}

What’s Next

Now let’s discuss what happens when the manager chooses a different entity and fills the call result.