Опыт интеграции с AMO CRM part 2

Создание сделки на АМО

При добавлении заказа в магазине e-commerce платформа отправляет веб хук на заданный адрес. Собственно не так важно как и откуда вы получаете данные, главное их получить ))

Контроллер получает данные заказа что то с ними делает и заодно подготавливает массив для модели АМО. Но давайте по порядку.

  1. Подготовить массив для модели
  2. Инициация экземпляра класса
  3. Создание новой сделки на АМО

Итак пункт первый. Подготовка массива. Тут ничего особенного, но есть ряд граблей. Поля для сделок типа текстовых (левая колонка в амо при просмотре сделки) ограничены в размере, 255 символов. Для полей типа дата нужно передавать значение в unix формате, у меня это поле для доставки, и если вы используете это поле, то оно не может быть пустым, иначе будет ошибка при создании сделки.


            $statusId = '34990069'; // статус на амо заказ согласован

            $dataOrderAmo = [
                'order name'  => 'Ecwid #' . $orderId,
                'ekwidId'     => $orderId,
                'order price' => $orderEcwid['total'],
                'statusId'    => $statusId,
                'notes'       => '',
                'lang'        => $ecwidLang,
                'refer_URL'   => $globalReferer,
                'name'        => $orderEcwid['shippingPerson']['name'],
                'email'       => $orderEcwid['email'],
                'phone'       => $orderEcwid['shippingPerson']['phone'],
                'order id'    => $orderId,
                'address'     => $address,
                'payment'     => $payment,
                'date'        => $dateOrder,
                'time'        => $timeDelivery,
                'orders'      => $orders
            ];

Пункт второй. Инициация экземпляра класса.

$amo = new Amo();

При этом конструктор создает экземпляр объекта. Стоит отметить что конструктор берет данные из файлов которые создаются в процессе получения или обновления токенов доступа, про подробности авторизации и получение токенов можно прочитать в статье Опыт интеграции с AMO CRM part 1 . А также использует статический метод для получения даты окончания действия токена.

И наконец Пункт третий. Создание новой сделки на АМО.

$amoRes = $amo->NewOrder($dataOrderAmo);

Теперь давайте посмотрим внимательней что при этом происходит в этой функции.
1. инициализация апи клиента

// инициализация апи клиента
$apiClient = $this->getApiClient();

Апи клиент реализует функция getApiClient Она же проверяет актуальность токена и при необходимости обновляет его.

Далее идет сам процесс создания сделки. Основа взята из примера амо комплексное добавление сделки. Выбрал его чтоб сразу и клиента прикреплять к сделке. *В примере можно также прикрепить компанию.

Сама сделка на АМО может иметь множество самых разных полей и прикрепленных объектов. Но основные для нас сейчас это боковые поля и прикрепленный контакт.
Ниже скрин из АМО

Теперь по порядку что где сверху вниз по скрину.

  1. Имя сделки
  2. Теги сделки
  3. Статус или воронка сделки
  4. Бюджет сделки
  5. Идентификатор заказа на e-commerce платформе *(для сокращения кода в примере оставлены поля только разных типов) Тут мы впервые сталкиваемся с кастомными полями. Это кастомное поле текстового типа, для добавления другого подобного поля можно просто скопировать код и поменять id поля (посмотреть id поля можно нажав на кнопку настроить, 14 на скрине, и затем выбрать нужное поле из списка)
  6. Статус оплаты *кастомное поле типа список
  7. Дата доставки (или самовывоза) *кастомное поле типа дата
  8. Время доставки *кастомное поле типа список с мульти выбором (тут я получаю от платформы только одно значение, поэтому реализацию установки нескольких значений не реализовывал)
  9. Телефон получателя *см пункт 5
  10. Имя получателя *см пункт 5
  11. Адрес получателя *см пункт 5
  12. Источник заказа *см пункт 5
  13. *по этой кнопке можно посмотреть id поля и его тип *см скрин ниже

Далее прикрепляем к сделке клиента и его контактные данные *см скрин ниже

  1. Имя контакта *а примере есть имя и фамилия, но я использовал только одно поле имя так как приходит только в одном поле от платформы.
  2. телефон
  3. почта
  4. язык *кастомное поле типа список

Также для облегчения отладки есть функция получения заказа АМО по его id getOrderById. И функция вывода ошибок printError.

Теперь модель АМО выглядит так

<?php
namespace App\Model;

// здесь немного больше чем нужно на данный момент но понадобится позднее
use AmoCRM\Client\AmoCRMApiClient;
use AmoCRM\Collections\CatalogElementsCollection;
use AmoCRM\Collections\ContactsCollection;
use AmoCRM\Collections\CustomFieldsValuesCollection;
use AmoCRM\Collections\Leads\LeadsCollection;
use AmoCRM\Collections\LinksCollection;
use AmoCRM\Collections\NotesCollection;
use AmoCRM\Collections\TagsCollection;
use AmoCRM\Exceptions\AmoCRMApiException;
use AmoCRM\Helpers\EntityTypesInterface;
use AmoCRM\Models\CatalogElementModel;
use AmoCRM\Models\ContactModel;
use AmoCRM\Models\CustomFieldsValues\DateTimeCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\MultiselectCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\MultitextCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\PriceCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\SelectCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\StreetAddressCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\TextCustomFieldValuesModel;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\DateCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\MultiselectCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\MultitextCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\PriceCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\SelectCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\StreetAddressCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueCollections\TextCustomFieldValueCollection;
use AmoCRM\Models\CustomFieldsValues\ValueModels\DateCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\MultiselectCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\MultitextCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\PriceCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\SelectCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\StreetAdressCustomFieldValueModel;
use AmoCRM\Models\CustomFieldsValues\ValueModels\TextCustomFieldValueModel;
use AmoCRM\Models\LeadModel;
use AmoCRM\Models\NoteType\CommonNote;
use AmoCRM\Models\NoteType\ServiceMessageNote;
use AmoCRM\Models\TagModel;
use League\OAuth2\Client\Token\AccessToken;

class Amo
{
    private static $secretKey = AMO_SECRET_KEY;
    private static $integrationId = AMO_INTEGRATION_ID;
    private static $requestUrl = AMO_APP_BACKURL;

    private $assetsToken;
    private $refreshToken;
    private $baseDomain;
    private $endDate; 
    private $expires_in; // используется для создания клиента апи

    public function __construct()
    {
        $amoData['ataautorise'] = json_decode(file_get_contents(AMO_AUTORISE_FILE), true);
        $amoData['tokens'] = json_decode(file_get_contents(AMO_TOKEN_MODEL_FILE), true);

        $this->assetsToken  = $amoData['tokens']['access_token'];
        $this->endDate      = $this::getEndDate($amoData['tokens']['expires_in']);
        $this->baseDomain    = $amoData['ataautorise']['referer'];
        $this->refreshToken = $amoData['tokens']['refresh_token'];
        $this->expires_in   = $amoData['tokens']['expires_in'];
    }

    // пычисление времени жизни токена для конструктора
    private static function getEndDate($time)
    {
        // дата записи файла происходит в момент обновления токена
        // поетому берем её за стартовую точку отсчета
        $date = date('Y-m-d H:m:s', filectime(AMO_TOKEN_MODEL_FILE));
        $dateFile = new \DateTime($date);
        // добавляем время жизни токена
        $dateFile->modify('+' . $time - 1 . ' sec');

        // возвращаем конечную дату окончания действия токена
        return $dateFile->format('Y-m-d H:i:s');
    }

    // первичное получение и обновление токена
    public static function getTokens(array $AmoDate, $mode)
    {

        $subdomain = $AmoDate['referer']; //Поддомен нужного аккаунта
        $link = 'https://' . $subdomain . '/oauth2/access_token'; //Формируем URL для запроса

        /** Соберем данные для запроса */
        $data = [
            'client_id' => self::$integrationId,
            'client_secret' => self::$secretKey,
            'grant_type' => $mode,
            'redirect_uri' => AMO_APP_BACKURL,
        ];

        // модификация запроса для получения или обновления токенов
        if ($mode == 'refresh_token') {
            $data['refresh_token'] = $AmoDate['refresh_token'];
        } else {
            $data['code'] = $AmoDate['code'];
        }


        /**
         * Нам необходимо инициировать запрос к серверу.
         * Воспользуемся библиотекой cURL (поставляется в составе PHP).
         * Вы также можете использовать и кроссплатформенную программу cURL, если вы не программируете на PHP.
         */
        $curl = curl_init(); //Сохраняем дескриптор сеанса cURL
        /** Устанавливаем необходимые опции для сеанса cURL  */
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl,CURLOPT_USERAGENT,'amoCRM-oAuth-client/1.0');
        curl_setopt($curl,CURLOPT_URL, $link);
        curl_setopt($curl,CURLOPT_HTTPHEADER,['Content-Type:application/json']);
        curl_setopt($curl,CURLOPT_HEADER, false);
        curl_setopt($curl,CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($curl,CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($curl,CURLOPT_SSL_VERIFYPEER, 1);
        curl_setopt($curl,CURLOPT_SSL_VERIFYHOST, 2);
        $out = curl_exec($curl); //Инициируем запрос к API и сохраняем ответ в переменную
        $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);


        /** Теперь мы можем обработать ответ, полученный от сервера. Это пример. Вы можете обработать данные своим способом. */
        $code = (int)$code;

        $errors = [
            400 => 'Bad request',
            401 => 'Unauthorized',
            403 => 'Forbidden',
            404 => 'Not found',
            500 => 'Internal server error',
            502 => 'Bad gateway',
            503 => 'Service unavailable',
        ];

        /** Если код ответа не успешный - возвращаем сообщение об ошибке  */
        if ($code < 200 || $code > 204) {

            if (!empty($errors[$code])) {
                echo "error $code " . $errors[$code] . '<br>';
            }
            $res = json_decode($out);

            foreach ($res as $k => $val) {
                echo "<p>$k - $val</p>";
            }

        } else {
            /**
             * Данные получаем в формате JSON
             */


            file_put_contents(AMO_TOKEN_MODEL_FILE, $out);

            return $out;
        }

    }

    // обновление токена доступа
    private function refreshToken()
    {
        $amoData = [
            'referer' => $this->baseDomain,
            'refresh_token' => $this->refreshToken,
        ];

        var_dump('amoData', $amoData);

        $res = Amo::getTokens($amoData, 'refresh_token');
        $res = json_decode($res, true);

        $this->refreshToken = $res['refresh_token'];
        $this->assetsToken  = $res['access_token'];
        $this->endDate      = $this::getEndDate($res['expires_in']);

        return true;
    }


    // создание сделки на амо для прода
    public function NewOrder(array $amoData)
    {
        // инициализация апи клиента
        $apiClient = $this->getApiClient();


        ///////////////////////////////////////////////////////////////////////////
        // текстовый тип кастомного поля
         $textCustomFieldValueModel = new TextCustomFieldValuesModel();
        $textCustomFieldValueModel->setFieldId(489653);
        $textCustomFieldValueModel->setValues(
            (new TextCustomFieldValueCollection())
                ->add((new TextCustomFieldValueModel())->setValue($amoData['ekwidId']))
        );
        $leadCustomFieldsValues->add($textCustomFieldValueModel);


        // тип поля список
        $selectCustomFieldValueModel = new SelectCustomFieldValuesModel();
        $selectCustomFieldValueModel->setFieldId(308363);
        $selectCustomFieldValueModel->setValues(
            (new SelectCustomFieldValueCollection())
                ->add((new SelectCustomFieldValueModel())->setValue($amoData['payment']))
        );
        $leadCustomFieldsValues->add($selectCustomFieldValueModel);


        // тип поля дата-время
        $dateCustomFieldValueModel = new DateTimeCustomFieldValuesModel();
        $dateCustomFieldValueModel->setFieldId(462257);
        $dateCustomFieldValueModel->setValues(
            (new DateCustomFieldValueCollection())
                ->add((new DateCustomFieldValueModel())->setValue($amoData['date']))
        );
        $leadCustomFieldsValues->add($dateCustomFieldValueModel);


        // тип поля список с мульти выбором
        $timeCustomFieldValueModel = new MultiselectCustomFieldValuesModel();
        $timeCustomFieldValueModel->setFieldId(462331);
        $timeCustomFieldValueModel->setValues(
            (new MultiselectCustomFieldValueCollection())
                ->add((new MultiselectCustomFieldValueModel())->setValue($amoData['time']))
        );
        $leadCustomFieldsValues->add($timeCustomFieldValueModel);


        // тип поля адрес
        $adressCustomFeeldValueModel = new StreetAddressCustomFieldValuesModel();
        $adressCustomFeeldValueModel->setFieldId(308403);
        $adressCustomFeeldValueModel->setValues(
            (new StreetAddressCustomFieldValueCollection())
                ->add((new StreetAdressCustomFieldValueModel())->setValue($amoData['address']))
        );
        $leadCustomFieldsValues->add($adressCustomFeeldValueModel);

       //// добавление наименований заказа в теги
        $lead = new LeadModel();

        if (!empty($amoData['orders'])) {

            //Создадим тег
            $tagsCollection = new TagsCollection();


            foreach ($amoData['orders'] as $item) {
                $tag = new TagModel();
                $tag->setName($item);
                $tagsCollection->add($tag);

            }
            $lead->setTags($tagsCollection);

        }

        ///

        $lead->setName($amoData['order name'])
            ->setPrice($amoData['order price'])
            ->setCustomFieldsValues($leadCustomFieldsValues) // прикрепление к сделке значений из полей выше
            ->setStatusId($amoData['statusId'])
            ->setContacts( // добавляем к сделке контакт с данными
                (new ContactsCollection())
                    ->add(
                        (new ContactModel())
                            ->setFirstName($amoData['name'])
                            ->setLastName($amoData['last name'])
                            ->setIsMain(true)
                            ->setCustomFieldsValues(
                                (new CustomFieldsValuesCollection())
                                    ->add(
                                        (new MultitextCustomFieldValuesModel())
                                            ->setFieldCode('PHONE')
                                            ->setValues(
                                                (new MultitextCustomFieldValueCollection())
                                                    ->add(
                                                        (new MultitextCustomFieldValueModel())
                                                            ->setValue($amoData['phone'])
                                                    )
                                            )
                                    )
                                    ->add(
                                        (new MultitextCustomFieldValuesModel())
                                            ->setFieldCode('EMAIL')
                                            ->setValues(
                                                (new MultitextCustomFieldValueCollection())
                                                    ->add(
                                                        (new MultitextCustomFieldValueModel())
                                                            ->setValue($amoData['email'])
                                                    )
                                            )
                                    )
                                    ->add( // кастомное поле для языка 
                                        (new SelectCustomFieldValuesModel())
                                            ->setFieldId(490441)
                                            ->setValues(
                                                (new SelectCustomFieldValueCollection())
                                                    ->add(
                                                        (new SelectCustomFieldValueModel())
                                                            ->setValue($amoData['lang'])
                                                    )
                                            )
                                    )
                            )
                    )
            )
            ->setRequestId($amoData['order id']);

        /////////////////////////////////////////////////////////////////////////////////////////

        $leadsCollection = new LeadsCollection();
        $leadsCollection->add($lead);

        try {
            $addedLeadsCollection = $apiClient->leads()->addComplex($leadsCollection);
        } catch (AmoCRMApiException $e) {
            $this->printError($e);
            die;
        }
        

        /** @var LeadModel $addedLead */
        foreach ($addedLeadsCollection as $addedLead) {
            //Пройдемся по добавленным сделкам и выведем результат
            $leadId = $addedLead->getId();
            $contactId = $addedLead->getContacts()->first()->getId();

            $res= [
                'amo_id'    => $leadId,
                'client_id' => $contactId
            ];
        }

        return $res;

    }

   

    // получение заказа с амо по его ид, (для отладки)
    public function getOrderById($id)
    {
        $apiClient = $this->getApiClient();


        $lead = $apiClient->leads()->getOne($id, [LeadModel::CONTACTS, LeadModel::CATALOG_ELEMENTS]);

        $leadNotesService = $apiClient->notes(EntityTypesInterface::LEADS);
        $notesCollection = $leadNotesService->getByParentId($id);

        echo '<hr> note collection';
        var_dump($notesCollection);
        echo '<hr>';

        // катомные поля для сделок
        $customFieldsService = $apiClient->customFields(EntityTypesInterface::LEADS);
        echo '<hr> custom feeld collection <br>';
        var_dump($customFieldsService);
        echo '<hr>';


        return $lead;
    }

    // апи клиент,
    // также проверяет дату действия токена
    // и если надо обновляет его
    public function getApiClient()
    {
        $date = new \DateTime(Date('Y-m-d H:i:s'));
        $endDate = new \DateTime($this->endDate);

        if ($date >= $endDate) {
            $this->refreshToken(); 
        } else {
            var_dump('token actual');
        }


        $apiClient = new AmoCRMApiClient(self::$integrationId, self::$secretKey, self::$requestUrl);

        $accessToken = new AccessToken([
            'access_token' => $this->assetsToken,
            'expires_in' => $this->expires_in
        ]);

        $subdomain = $this->baseDomain;
        $apiClient->setAccessToken($accessToken)
            ->setAccountBaseDomain($subdomain);


        return $apiClient;
    }


    public function printError(AmoCRMApiException $e): void 
    {
        $errorTitle = $e->getTitle();
        $code = $e->getCode();
        $debugInfo = var_export($e->getLastRequestInfo(), true);

        $error = <<<EOF
Error: $errorTitle
Code: $code
Debug: $debugInfo
EOF;

        echo '<pre>' . $error . '</pre>';
    }

}

А в следующей статье я рассмотрю прикрепление к сделке товаров и ленивое создание списка товаров.

Сдедующая статья
Google Calendar API
Предыдущая статья
Опыт интеграции с AMO CRM part 1