Небольшая интеграция miniShop2 с сервисом iiko

Добрый день.
В этой статье будет затронута тема минимальной интеграции магазина miniShop2 с специализированным сервисом для кафе/ресторанов iiko. Интеграция это пожалуй громкое слово, так как расскажу только о передаче заказов в систему, а не полная синхронизация номенклатуры и заказчиков.



Achtung! Николай в комментариях дал отличное решение нижеописанного плагина. Код решения оставлю для примера, можно будет надергать кусочками для своих нужд.

В моем случае была задача создавать доставку в системе, чтобы передавался тип доставки, способ оплаты, адрес доставки, сами товары заказа.

Нам понадобится
  • магазин на miniShop2
  • API ключ (или apiLogin) из системы (мне его передал заказчик)
  • Документация


С помощью apiLogin нам необходимо получить токен, который действителен 1 час. Для просто-то всего вот этого хранение и проверка токена не описана.
Можно использовать 2 варианта работы с api, через программу Postman, либо через console/modalConsole самого MODx.
Приведу простенький код для получения токена через php (я использовал Postman)
$apiLogin = 'xxx-xxxx-xx'; // исходные данные, предоставляются системой
$params = ['apiLogin' => $apiLogin];     
  
$curl = curl_init();
curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://api-ru.iiko.services/api/1/access_token',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => '',
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => json_encode($params),
  CURLOPT_HTTPHEADER => array(
    'Content-Type: application/json'
  ),
));

$response = curl_exec($curl);
curl_close($curl);
 
$response_arr = json_decode($response, true);

if($response_arr['token']) {
    $token = $response_arr['token'];
} else {
    echo 'Error get token'; 
}

После получения токена нам необходимо получить еще 2 параметра: organizationId и terminalGroupId. Они необходимо в дальнейшем для получения id оплат и создания заказа. Данные параметры являются постоянными и проще их один раз получить и сохранить, чем запрашивать постоянно.

organizationId можно получить по адресу
api-ru.iiko.services/api/1/organizations
Ссылка на домументацию:
api-ru.iiko.services/#tag/Organizations

По сути описывать все эти манипуляции с получением промежуточных данных нет смысла, так как в родной документации все максимально подробно. Расскажу только про основные шаги.

Далее необходимо получить terminalGroupId. Принцип тот же, запрос на адрес api-ru.iiko.services/api/1/terminal_groups
Но в теле запроса уже должен быть json с organizationIds (важно, в некоторых случаях названия параметров отличаются: organizationId и organizationIds так как в системе может быть несколько точек продаж, в моем случае 1 точка и данные я получаю только для нее)

Также важно сопоставить ID способов оплаты
Не забываем что в Postman в header должны быть указаны параметры Content-Type = application/json, а в авторизации выбран тип Bearer Token и ваш свежий токен (помните, он годен всего час)
Получим из iiko все варианты:


Отсюда мы берем 2 параметра id и paymentTypeKind
Все эти параметры, кроме токена, являются постоянными и их можно получить один раз на все время. Только если со временем появляются новые методы оплаты или точки продажи, тогда уже надо вносить изменения.

Также важно чтобы ваши товары и товары в системе были сопоставлены по id из системы, то есть я создал поле у товара и внес туда id из iiko: iiko_product_id. Так как в саму систему нельзя передать просто название товара, то приходится делать такие привязки. Если какое-то значение не совпадет, то система будет выдавать ошибку.
После получения всех необходимых данных можно приступить к написанию плагина для минишопа.

Создаем плагин на событие msOnCreateOrder

<?php
if ($modx->event->name=='msOnCreateOrder') {
    
    //////////////// 
    $apiLogin           = 'xxx-xxxxx-xx'; // выдется заказчиком
    $organizationId     = "xxxxxx-xxxxxxx-xx-xxxxx"; // получаем один раз через api 
    $terminalGroupId    = "xxxxxx-xxxxxxx-xx-xxxxx"; // получаем один раз через api 
    $city_id            = "xxxxxx-xxxxxxx-xx-xxxxx"; // получаем один раз через api 
    $token              = false;
    ///////////////
    
    if (!function_exists('getResponse')) {
        
        function getResponse($action, $params, $auth = false, $token = '') {
            
            $url = 'https://api-ru.iiko.services/'.$action;
            if($auth) {
                $auth = 'Authorization: Bearer '.$token;
            } else {
                $auth = '';
            }
            
            $curl = curl_init();
            curl_setopt_array($curl, array(
              CURLOPT_URL => $url,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_ENCODING => '',
              CURLOPT_MAXREDIRS => 10,
              CURLOPT_TIMEOUT => 0,
              CURLOPT_FOLLOWLOCATION => true,
              CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
              CURLOPT_CUSTOMREQUEST => 'POST',
              CURLOPT_POSTFIELDS => json_encode($params),
              CURLOPT_HTTPHEADER => [
                'Content-Type: application/json', 
                $auth
              ],
            ));
            
            $response = curl_exec($curl);
            curl_close($curl);
            return $response;
        }
    }

    
    // get token 
    $params = ['apiLogin' => $apiLogin];
    $tokenJson = getResponse('api/1/access_token', $params); 
    
    if($tokenJson) {
        $response_arr = json_decode($tokenJson, true);
    
        if($response_arr['token']) {
            
            $token = $response_arr['token'];
            
        } else {
            $modx->log(1,'[iiko]: Error to get token '.print_r($response_arr, 1)); 
            return;
        }
    
    } 
     
    
    // если нет токена, то ничего и не отправляем
    if($token) {
        
        $data       = [];
        $customer   = []; 
        $products   = [];
        $address    = [];
        
        $orderData = [
            'order'         => $msOrder->toArray(),
            'delivery'      => $msOrder->Delivery->toArray(),
            'payment'       => $msOrder->Payment->toArray(),
            'address'       => $msOrder->Address->toArray(),
            'user'          => $msOrder->User->toArray(),
            'user_profile'  => $msOrder->UserProfile->toArray(),
        ];
        
        //$modx->log(1,'Order data '.print_r($orderData, 1));         
        
        // получаем данные покупателя 
        $customer['name']   = $orderData['address']['receiver'];   
        $customer['email']  = $orderData['user_profile']['email']; 
        $data['customer']   = $customer;
        
        
        // получаем телефон заказчика 
        // Телефон должен передаваться с кодом страны
        $data['phone'] = '+7'.$orderData['address']['phone']; 
        $data['phone'] = str_replace('+7+7', '+7', $data['phone']);
        
        
        // уникальный идентификатор, чтобы отличать в системе заказы
        $data['sourceKey'] = 'Site online';
        
        
        // информация о способе оплаты
        $payment_id 	= $orderData['order']['payment'];
        switch ($payment_id) {
            case 1:
                $paymentType    = 'Cash'; // наличка
                $paymentTypeId  = 'xxxxxx-xxxxxxx-xxx-xxxxxx-xx';// получаем один раз через api 
                $payTitle       = 'Наличные';
                break;
            case 3:
                $paymentType    = 'Card'; // карта курьеру
                $paymentTypeId  = 'exxxxxx-xxxxxxx-xxx-xxxxxx-xx';// получаем один раз через api 
                $payTitle       = 'Картой курьеру';
                break;
            case 4:
                $paymentType    = 'Card'; // онлайн оплата
                $paymentTypeId  = 'xxxxxx-xxxxxxx-xxx-xxxxxx-xx';// получаем один раз через api 
                $payTitle       = 'Онлайн оплата на сайте';
                break;
        }
        
        
        //
        $data['payments'][] = [
            'paymentTypeKind'   => $paymentType,
            'sum'               => $orderData['order']['cost'],
            'paymentTypeId'     => $paymentTypeId
        ];
        
        
        //////////////////////
        
        // товары заказа
        if($orderProducts = $msOrder->getMany('Products') ) { 
            
            $i = 0;

	        foreach ($orderProducts as $orderProduct) {
	            
	            if($fields = $modx->getObject('msProductData', array('id' => $orderProduct->get('product_id') ))){ 

	            	$fields_array = $fields->toArray(); 

		            if($fields_array['iiko_product_id']) { 

			            $products[$i]['productId']= $fields_array['iiko_product_id'];
						$products[$i]['price'] 	  = $orderProduct->get('price');
			            $products[$i]['amount']   = $orderProduct->get('count'); 
						$products[$i]['type'] 	  = 'Product'; 
 
	                    $i++;
	             
		            }

	            }
	            
	        }

        }
        
        
        
        //////////////////////////////////
        // получение адреса заказчика
        
        /*
        // тут описан вариант как получить ID улиц адреса из системы iiko.
        // надо лишь через api получить 1 раз json всех улиц и потом искать по нему
        if($orderData['address']['street']) { 
            $file_path = '/assets/streets.json';
            $streetsJson = file_get_contents($modx->getOption('base_path')."/assets/streets.json");
            
            if($streetsJson) {
                $streetsArr = json_decode($streetsJson, true);
                $streets = $streetsArr['streets'];
            
                foreach($streets as $street) {
                    if($street['name'] == $orderData['address']['street']) {
                        $street_id = $street['id'];
                        break;
                    }
                }
                 
            } 
            
        } else {
            
            $street_id = false;
            
        }

        // Важно! нельзя передавать и id и название улицы одновременно
        
        if($street_id) {
            $street  = [
                'id' => $street_id, 
                'city' => $orderData['address']['city'] ?: 'Не указан'
            ];
        } else {
            $street  = [
                'name' => 'Не указана', 
                'city' => $orderData['address']['city'] ?: 'Не указан'
            ];
        }
        */
        
        
        if($orderData['delivery']['id'] > 1) { 
                
    		 //Координаты доставки
             // если передается улица и номер дома, то нет необходимости передавать координаты
    		 /*
    		 $coordinates = [
    		     'latitude' => 0,
    		     'longitude' => 0
    		 ]; 
    		 */
        
            /////////////////////// 
            
            // если один город для доставки, то город может быть не обязателен 
            $street  = [ 
                'name' => $orderData['address']['street'] ?: 'Не указана',
                //'city' => $orderData['address']['city'] ?: 'Не указан'
            ];
             
            /////////////////////// 
    
            $address = [
                'street' =>  $street, // улица - массив 
                'house' => $orderData['address']['building'] ?: '-', // номер дома
                'flat' => $orderData['address']['room'] ?: '-', // номер квартиры
                'entrance' => $orderData['address']['entrance'] ?: '-' // подъезд
            ];
             
            /////////////////////// 
        
            $data['deliveryPoint'] = [
                //'coordinates' => $coordinates,
                'address' => $address    
            ];
            
        } 
        
        
        ///////////////////////////////////
        $deliveryInfo = false;
        $orderServiceType = false;
        $has_delivery = false;
        
        switch ($orderData['delivery']['id']) {
            case 1: 
                $deliveryInfo       = 'Доставка: Самовывоз';
                $orderServiceType   = 'DeliveryByClient'; // Важно - отличается от курьерской доставки
                $delivery_id = 'xxxxxx-xxxxxxx-xxx-xxxxxx-xx'; // получаем один раз через api 
                $delivery_cost = 100;
                break;
            case 2:
                $deliveryInfo       = 'Доставка: Предзаказ на время '.$orderData['address']['metro'];
                $orderServiceType   = 'DeliveryByCourier';
                $delivery_id = 'xxxxxx-xxxxxxx-xxx-xxxxxx-xx'; // получаем один раз через api 
                $delivery_cost = 100;
                break;
            case 3:
                $deliveryInfo       = 'Доставка: Доставка по адресу';
                $orderServiceType   = 'DeliveryByCourier';
                $delivery_id = 'xxxxxx-xxxxxxx-xxx-xxxxxx-xx'; // получаем один раз через api 
                $delivery_cost = 100;
                break;
        }
        
        ////////////////////////////////
        

        // оказалось что доставка тоже является товаром        
    
        // добавить доставку - товар
        $products[$i] = [
            'productId'     => $delivery_id,
            'price' 	    => $delivery_cost,
            'amount'        => 1,
            'type'	        => 'Product'
        ]; 
        
      
        ////////////////////////////////
        
        if(count($products)>0){
            $data['items'] = $products;
        }
        
        ////////////////////////////////
        
        
        $data['comment'] = $orderData['address']['comment']. " Способ оплаты: ".$payTitle.". ".$deliveryInfo ;
        $data['sum'] = $orderData['order']['cost'];
         
         
        $data['orderServiceType'] = $orderServiceType;
        
        
        // передаем заказ в iiko 
        $params = [
            'organizationId' => $organizationId,
            'terminalGroupId' => $terminalGroupId,
            'order' => $data
        ];
        
        
        //$modx->log(1,'iiko params  --- '.print_r(json_encode($params), 1));
        $createOrderJson = getResponse('api/1/deliveries/create', $params, true, $token); 
        
        // логируем в отдельный файл все запросы для дальнейшей отладки
        $modx->log(1, 'Отправленные параметры: '.json_encode($params) , [
            "target" => "FILE",   
            "options" => [
                "filename" => "iiko.log", // Имя файла 
            ]
        ]);
        

        // логируем в отдельный файл все ответы для дальнейшей отладки
        $modx->log(1, 'Полученный ответ: '.$createOrderJson , [
            "target" => "FILE",  
            "options" => [
                "filename" => "iiko.log", // Имя файла 
            ]
        ]);
        
        //$modx->log(1,'Create response  --- '.print_r($createOrderJson, 1));
    
    }
}
Статья дает небольшой толчек для написания полной интеграции. Мою задачу это решило.
Вопросы по системе iiko мне задавать бесполезно, так как тут и так описаны все мои действия и больше чем описано в документации я вам врятли расскажу.
Евгений Webinmd
19 января 2022, 00:14
modx.pro
3
688
+5

Комментарии: 3

Николай Савин
19 января 2022, 10:49
0
Жень, нельзя делать плагины с таким количеством кода и тем более с функциями.
Нужно переносить в классы. Бить на методы. Это прям антипаттерн. Нужен серьезный рефакторинг.
    Евгений Webinmd
    19 января 2022, 11:48
    +1
    там по большому счету много что надо было делать по другому. Твой совет учту на будущее, код оставлю для потомков, пускай знают как делать НЕ надо
    Николай Савин
    19 января 2022, 13:27
    +4
    Набросал небольшой рефакторинг этого кода
    Упростил код плагина, перенес содержимое в компонент
    github.com/biz87/iiko

    Код по-прежнему хромает, но первый шаг к оптимизации сделан.
      Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
      3