Создание сделки на АМО
При добавлении заказа в магазине e-commerce платформа отправляет веб хук на заданный адрес. Собственно не так важно как и откуда вы получаете данные, главное их получить ))
Контроллер получает данные заказа что то с ними делает и заодно подготавливает массив для модели АМО. Но давайте по порядку.
- Подготовить массив для модели
- Инициация экземпляра класса
- Создание новой сделки на АМО
Итак пункт первый. Подготовка массива. Тут ничего особенного, но есть ряд граблей. Поля для сделок типа текстовых (левая колонка в амо при просмотре сделки) ограничены в размере, 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 Она же проверяет актуальность токена и при необходимости обновляет его.
Далее идет сам процесс создания сделки. Основа взята из примера амо комплексное добавление сделки. Выбрал его чтоб сразу и клиента прикреплять к сделке. *В примере можно также прикрепить компанию.
Сама сделка на АМО может иметь множество самых разных полей и прикрепленных объектов. Но основные для нас сейчас это боковые поля и прикрепленный контакт.
Ниже скрин из АМО
Теперь по порядку что где сверху вниз по скрину.
- Имя сделки
- Теги сделки
- Статус или воронка сделки
- Бюджет сделки
- Идентификатор заказа на e-commerce платформе *(для сокращения кода в примере оставлены поля только разных типов) Тут мы впервые сталкиваемся с кастомными полями. Это кастомное поле текстового типа, для добавления другого подобного поля можно просто скопировать код и поменять id поля (посмотреть id поля можно нажав на кнопку настроить, 14 на скрине, и затем выбрать нужное поле из списка)
- Статус оплаты *кастомное поле типа список
- Дата доставки (или самовывоза) *кастомное поле типа дата
- Время доставки *кастомное поле типа список с мульти выбором (тут я получаю от платформы только одно значение, поэтому реализацию установки нескольких значений не реализовывал)
- Телефон получателя *см пункт 5
- Имя получателя *см пункт 5
- Адрес получателя *см пункт 5
- Источник заказа *см пункт 5
- *по этой кнопке можно посмотреть id поля и его тип *см скрин ниже
Далее прикрепляем к сделке клиента и его контактные данные *см скрин ниже
- Имя контакта *а примере есть имя и фамилия, но я использовал только одно поле имя так как приходит только в одном поле от платформы.
- телефон
- почта
- язык *кастомное поле типа список
Также для облегчения отладки есть функция получения заказа АМО по его 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>';
}
}
А в следующей статье я рассмотрю прикрепление к сделке товаров и ленивое создание списка товаров.