From cf88a8d7f8b91ee07bba062f26743a6547dabba3 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Tue, 22 Nov 2016 18:02:37 +0300 Subject: [PATCH] init --- LICENSE | 2 +- README.md | 35 + README.ru.md | 33 + classes/modules/RetailCRM/__admin.php | 241 +++ classes/modules/RetailCRM/__events.php | 57 + classes/modules/RetailCRM/class.php | 35 + .../classes/retailcrm/RCrmActions.php | 265 +++ .../classes/retailcrm/RCrmApiClient.php | 1825 +++++++++++++++++ .../classes/retailcrm/RCrmApiResponse.php | 164 ++ .../classes/retailcrm/RCrmCurlException.php | 5 + .../classes/retailcrm/RCrmHelpers.php | 175 ++ .../classes/retailcrm/RCrmHistory.php | 470 +++++ .../classes/retailcrm/RCrmHttpClient.php | 110 + .../RetailCRM/classes/retailcrm/RCrmIcml.php | 309 +++ .../classes/retailcrm/RCrmJsonException.php | 5 + .../RetailCRM/classes/retailcrm/RCrmProxy.php | 42 + .../classes/retailcrm/RetailcrmApiClient.php | 811 ++++++++ .../retailcrm/RetailcrmApiResponse.php | 122 ++ .../classes/retailcrm/RetailcrmHelpers.php | 41 + .../classes/retailcrm/RetailcrmHttpClient.php | 113 + .../classes/retailcrm/RetailcrmProxy.php | 39 + .../RetailCRM/classes/retailcrm/icml.php | 294 +++ classes/modules/RetailCRM/data/objects.xml | 100 + classes/modules/RetailCRM/events.php | 15 + classes/modules/RetailCRM/i18n.php | 28 + classes/modules/RetailCRM/install.php | 26 + images/cms/admin/mac/icons/big/RetailCRM.png | Bin 0 -> 3538 bytes .../cms/admin/mac/icons/medium/RetailCRM.png | Bin 0 -> 2582 bytes .../cms/admin/mac/icons/small/RetailCRM.png | Bin 0 -> 1202 bytes images/cms/admin/modern/icon/RetailCRM.png | Bin 0 -> 1529 bytes 30 files changed, 5361 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 README.ru.md create mode 100644 classes/modules/RetailCRM/__admin.php create mode 100644 classes/modules/RetailCRM/__events.php create mode 100644 classes/modules/RetailCRM/class.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmActions.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmApiClient.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmApiResponse.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmCurlException.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmHelpers.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmHistory.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmHttpClient.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmIcml.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmJsonException.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RCrmProxy.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiClient.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiResponse.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RetailcrmHelpers.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RetailcrmHttpClient.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/RetailcrmProxy.php create mode 100644 classes/modules/RetailCRM/classes/retailcrm/icml.php create mode 100644 classes/modules/RetailCRM/data/objects.xml create mode 100644 classes/modules/RetailCRM/events.php create mode 100644 classes/modules/RetailCRM/i18n.php create mode 100644 classes/modules/RetailCRM/install.php create mode 100644 images/cms/admin/mac/icons/big/RetailCRM.png create mode 100644 images/cms/admin/mac/icons/medium/RetailCRM.png create mode 100644 images/cms/admin/mac/icons/small/RetailCRM.png create mode 100644 images/cms/admin/modern/icon/RetailCRM.png diff --git a/LICENSE b/LICENSE index 55bfcd4..49e2a52 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 retailCRM +Copyright (c) 2016 RetailDriver LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad59ceb --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +UMI.CMS module +============== + +Module allows integrate UMI.CMS with [RetailCRM](http://www.retailcrm.pro) + +#### Features: + +* Export orders to retailCRM & fetch changes back +* Export product catalog into [ICML](http://www.retailcrm.pro/docs/Developers/ICML) format + +#### Setup + +* Copy directories "classes" & "images" into document root +* Go to /admin/config/modules +* Into "Modules" tab fill installation script path (classes/modules/RetailCRM/install.php) +* Press setup button +* Go to module page +* Fill you api url & api key +* Specify directories matching + +#### Setting product catalog export + +Add to cron: + +``` +* */4 * * * /usr/bin/php /path_to_site/public_html/cron.php RetailCRM icml +``` + +#### Getting changes in orders + +Add to cron: + +``` +*/7 * * * * /usr/bin/php /path_to_site/public_html/cron.php RetailCRM history +``` diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..be9232b --- /dev/null +++ b/README.ru.md @@ -0,0 +1,33 @@ +UMI.CMS module +============== + +Модуль интеграции UMI.CMS c [RetailCRM](http://www.retailcrm.ru) + +#### Модуль позволяет: + +* Экспортировать в CRM данные о заказах и клиентах и получать обратно изменения по этим данным +* Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) +* Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) + +#### Настройка + +* Скопируйте директории classes и images в корень сайта +* В разделе управления модулями (/admin/config/modules) укажите путь до инсталяционного файла: classes/modules/RetailCRM/install.php +* На странице настроек модуля введите API url и API ключ, после этого установите соответствие справочников + +#### Выгрузка каталога + +Добавьте в крон запись вида + +``` +* */4 * * * /usr/bin/php /path_to_site/public_html/cron.php RetailCRM icml +``` + +#### Получение изменение из RetailCRM + +Добавьте в крон запись вида + +``` +*/7 * * * * /usr/bin/php /path_to_site/public_html/cron.php RetailCRM history +``` + diff --git a/classes/modules/RetailCRM/__admin.php b/classes/modules/RetailCRM/__admin.php new file mode 100644 index 0000000..a35a6c7 --- /dev/null +++ b/classes/modules/RetailCRM/__admin.php @@ -0,0 +1,241 @@ +get('retailcrm', 'crmUrl'); + $apiKey = (string)$config->get('retailcrm', 'apiKey'); + + $params = array( + 'access' => array( + 'string:crmUrl' => $crmUrl, + 'string:apiKey' => $apiKey + ) + ); + + // Костыль для локализации полей + $translations = array(); + + if (!empty($apiKey) && !empty($crmUrl)) { + $api = new RCrmProxy( + $config->get('retailcrm', 'crmUrl'), + $config->get('retailcrm', 'apiKey'), + __DIR__ . '/../../../retailcrm.error.log' + ); + + if($api->paymentTypesList() !== false) { + /* + * Order Payment Types + */ + /** @var RCrmApiClient $api */ + $crmPaymentTypes = $api->paymentTypesList()->getPaymentTypes(); + $umiPaymentTypes = new selector('objects'); + $umiPaymentTypes->types('object-type')->name('emarket', 'payment'); + + $map = $this->getRelationMap($config->get('retailcrm', 'orderPaymentTypeMap')); + + $orderPaymentsMapping = array(); + foreach ($umiPaymentTypes->result() as $umiPaymentType) { + $umiPaymentTypeId = $umiPaymentType->getPropByName('payment_type_id')->getObjectId(); + $umiPaymentTypeName = $umiPaymentType->getName(); + + $translations['order-payment-type-' . $umiPaymentTypeId] = $umiPaymentTypeName; + + $orderPaymentsMapping['select:order-payment-type-' . $umiPaymentTypeId] = array(); + $orderPaymentsMapping['select:order-payment-type-' . $umiPaymentTypeId]['value'] = $this->getRelationByMap($map, + $umiPaymentTypeId); + + $orderPaymentsMapping['select:order-payment-type-' . $umiPaymentTypeId]['none'] = ''; + foreach ($crmPaymentTypes as $crmPaymentType) { + $orderPaymentsMapping['select:order-payment-type-' . $umiPaymentTypeId][$crmPaymentType['code']] = $crmPaymentType['name']; + } + } + $params['orderPaymentsMapping'] = $orderPaymentsMapping; + + /* + * Order Delivery Type + */ + $crmDeliveryTypes = $api->deliveryTypesList()->getDeliveryTypes(); + $umiDeliveryTypes = new selector('objects'); + $umiDeliveryTypes->types('object-type')->name('emarket', 'delivery'); + + $map = $this->getRelationMap($config->get('retailcrm', 'orderDeliveryTypeMap')); + + $orderDeliveryTypesMapping = array(); + foreach ($umiDeliveryTypes as $umiDeliveryType) { + $umiDeliveryTypeId = $umiDeliveryType->getId(); + $umiDeliveryTypeName = $umiDeliveryType->getName(); + + $translations['order-delivery-type-' . $umiDeliveryTypeId] = $umiDeliveryTypeName; + + $orderDeliveryTypesMapping['select:order-delivery-type-' . $umiDeliveryTypeId] = array(); + $orderDeliveryTypesMapping['select:order-delivery-type-' . $umiDeliveryTypeId]['value'] = $this->getRelationByMap($map, + $umiDeliveryTypeId); + + $orderDeliveryTypesMapping['select:order-delivery-type-' . $umiDeliveryTypeId]['none'] = ''; + foreach ($crmDeliveryTypes as $crmDeliveryType) { + $orderDeliveryTypesMapping['select:order-delivery-type-' . $umiDeliveryTypeId][$crmDeliveryType['code']] = $crmDeliveryType['name']; + } + + } + $params['orderDeliveryTypesMapping'] = $orderDeliveryTypesMapping; + + /* + * Order Payment Statuses + */ + $crmPaymentStatuses = $api->paymentStatusesList()->getPaymentStatuses(); + $umiPaymentStatuses = new selector('objects'); + $umiPaymentStatuses->types('object-type')->name('emarket', 'order_payment_status'); + + $map = $this->getRelationMap($config->get('retailcrm', 'orderPaymentStatusMap')); + + $orderPaymentStatusesMapping = array(); + foreach ($umiPaymentStatuses->result() as $umiPaymentStatus) { + $umiPaymentStatusId = $umiPaymentStatus->getId(); + $umiPaymentStatusName = $umiPaymentStatus->getName(); + + $translations['order-payment-status-' . $umiPaymentStatusId] = $umiPaymentStatusName; + + $orderPaymentStatusesMapping['select:order-payment-status-' . $umiPaymentStatusId] = array(); + $orderPaymentStatusesMapping['select:order-payment-status-' . $umiPaymentStatusId]['value'] = $this->getRelationByMap($map, + $umiPaymentStatusId); + + $orderPaymentStatusesMapping['select:order-payment-status-' . $umiPaymentStatusId]['none'] = ''; + foreach ($crmPaymentStatuses as $crmPaymentStatus) { + $orderPaymentStatusesMapping['select:order-payment-status-' . $umiPaymentStatusId][$crmPaymentStatus['code']] = $crmPaymentStatus['name']; + } + } + $params['orderPaymentStatusesMapping'] = $orderPaymentStatusesMapping; + + /* + * Order Statuses + */ + $crmOrderStatuses = $api->statusesList()->getStatuses(); + $umiOrderStatuses = new selector('objects'); + $umiOrderStatuses->types('object-type')->name('emarket', 'order_status'); + + $map = $this->getRelationMap($config->get('retailcrm', 'orderStatusMap')); + + $params['orderStatusesMapping'] = array(); + foreach ($umiOrderStatuses->result() as $umiOrderStatus) { + $translations['order-status-' . $umiOrderStatus->getPropByName('codename')->getValue()] = $umiOrderStatus->getName(); + + $params['orderStatusesMapping']['select:order-status-' . $umiOrderStatus->getPropByName('codename')->getValue()] = array(); + $params['orderStatusesMapping']['select:order-status-' . $umiOrderStatus->getPropByName('codename')->getValue()]['value'] = $this->getRelationByMap($map, + $umiOrderStatus->getPropByName('codename')->getValue()); + + $params['orderStatusesMapping']['select:order-status-' . $umiOrderStatus->getPropByName('codename')->getValue()]['none'] = ''; + foreach ($crmOrderStatuses as $crmOrderStatus) { + $params['orderStatusesMapping']['select:order-status-' . $umiOrderStatus->getPropByName('codename')->getValue()][$crmOrderStatus['code']] = $crmOrderStatus['name']; + } + } + + $params['guidesMapping']['select:country'] = array(); + $params['guidesMapping']['select:country']['value'] = $config->get('retailcrm', 'countryGuideId'); + $params['guidesMapping']['select:country']['none'] = ''; + $objectTypes = umiObjectTypesCollection::getInstance(); + foreach ($objectTypes->getGuidesList() as $guideId => $guideName) { + $params['guidesMapping']['select:country'][$guideId] = $guideName; + } + } else { + // TODO: Добавить вывод ошибки, что данные некорректные + $params['incorrect-data'] = array(); + } + } + + $mode = getRequest("param0"); + if ($mode == "do") { + $params = $this->expectParams($params); + + $config->set('retailcrm', 'crmUrl', $params['access']['string:crmUrl']); + $config->set('retailcrm', 'apiKey', $params['access']['string:apiKey']); + + if (!empty($params['access']['string:crmUrl']) && !empty($params['access']['string:apiKey'])) { + /* + * Order Statuses + */ + if (!empty($params['orderStatusesMapping'])) { + $orderStatusMap = array(); + foreach ($params['orderStatusesMapping'] as $umiOrderStatus => $crmOrderStatus) { + $umiOrderStatus = str_replace('select:order-status-', '', $umiOrderStatus); + $orderStatusMap[] = $umiOrderStatus . ' <-> ' . $crmOrderStatus; + } + $config->set('retailcrm', 'orderStatusMap', $orderStatusMap); + } + + /* + * Order Payment Types + */ + if (!empty($params['orderPaymentsMapping'])) { + $orderPaymentTypeMap = array(); + foreach ($params['orderPaymentsMapping'] as $umiOrderPaymentType => $crmOrderPaymentType) { + $umiOrderPaymentType = str_replace('select:order-payment-type-', '', $umiOrderPaymentType); + $orderPaymentTypeMap[] = $umiOrderPaymentType . ' <-> ' . $crmOrderPaymentType; + } + $config->set('retailcrm', 'orderPaymentTypeMap', $orderPaymentTypeMap); + } + + /* + * Order Payment Statuses + */ + if (!empty($params['orderPaymentStatusesMapping'])) { + $orderPaymentStatusMap = array(); + foreach ($params['orderPaymentStatusesMapping'] as $umiOrderPaymentStatus => $crmOrderPaymentStatus) { + $umiOrderPaymentStatus = str_replace('select:order-payment-status-', '', $umiOrderPaymentStatus); + $orderPaymentStatusMap[] = $umiOrderPaymentStatus . ' <-> ' . $crmOrderPaymentStatus; + } + $config->set('retailcrm', 'orderPaymentStatusMap', $orderPaymentStatusMap); + } + + /* + * Order Delivery Types + */ + if (!empty($params['orderDeliveryTypesMapping'])) { + $orderDeliveryTypeMap = array(); + foreach ($params['orderDeliveryTypesMapping'] as $umiOrderDeliveryType => $crmOrderDeliveryType) { + $umiOrderDeliveryType = str_replace('select:order-delivery-type-', '', $umiOrderDeliveryType); + $orderDeliveryTypeMap[] = $umiOrderDeliveryType . ' <-> ' . $crmOrderDeliveryType; + } + $config->set('retailcrm', 'orderDeliveryTypeMap', $orderDeliveryTypeMap); + } + + if (!empty($params['guidesMapping'])) { + foreach ($params['guidesMapping'] as $guideName => $guideId) { + $guideName = str_replace('select:', '', $guideName); + $config->set('retailcrm', $guideName . 'GuideId', $guideId); + } + } + } + + $this->chooseRedirect(); + } + + $this->setDataType("settings"); + $this->setActionType("modify"); + + $data = $this->prepareData($params, "settings"); + + // Реалзиация костыля для локализации полей + foreach ($data['nodes:group'] as $groupKey => $group) { + foreach ($group['nodes:option'] as $optionKey => $option) { + $label = $option['attribute:label']; + + if (strpos($label, 'option-') > -1) { + $optionName = str_replace('option-', '', $label); + + if (isset($translations[$optionName])) { + $data['nodes:group'][$groupKey]['nodes:option'][$optionKey]['attribute:label'] = $translations[$optionName]; + } + } + + } + } + + $this->setData($data); + $this->doData(); + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/__events.php b/classes/modules/RetailCRM/__events.php new file mode 100644 index 0000000..1cc3956 --- /dev/null +++ b/classes/modules/RetailCRM/__events.php @@ -0,0 +1,57 @@ +generateICML(); + } + + public function onCronSyncHistory() + { + $history = new RCrmHistory(); + $history->runCustomers(); + $history->runOrders(); + } + + public function onOrderStatusChanged(umiEventPoint $eventPoint) + { + if ($eventPoint->getMode() != 'after') { + return; + } + + $mode = $eventPoint->getParam('old-status-id') == null ? 'create' : 'edit'; + + /** @var order $order */ + $order = $eventPoint->getRef('order'); + + RCrmActions::orderSend($order->getId(), $mode); + } + + public function onModifyProperty(umiEventPoint $eventPoint) + { + /** @var umiEventPoint $eventPoint */ + if ($eventPoint->getMode() != 'after') { + return; + } + + /** @var umiObject $entity */ + $entity = $eventPoint->getRef('entity'); + + RCrmActions::orderSend($entity->getId(), 'edit'); + } + + public function onModifyObject(umiEventPoint $eventPoint) + { + /** @var umiEventPoint $eventPoint */ + if ($eventPoint->getMode() != 'after') { + return; + } + + /** @var umiObject $object */ + $object = $eventPoint->getRef('object'); + + RCrmActions::orderSend($object->getId(), 'edit'); + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/class.php b/classes/modules/RetailCRM/class.php new file mode 100644 index 0000000..66f0636 --- /dev/null +++ b/classes/modules/RetailCRM/class.php @@ -0,0 +1,35 @@ +getCurrentMode() == "admin") { + $this->__loadLib("__admin.php"); + $this->__implement("__RetailCRM_adm"); + } + + $this->__loadLib("../emarket/includes.php"); + + // RetailCRM classes + $this->__loadLib("classes/retailcrm/RCrmActions.php"); + $this->__loadLib("classes/retailcrm/RCrmApiClient.php"); + $this->__loadLib("classes/retailcrm/RCrmApiResponse.php"); + $this->__loadLib("classes/retailcrm/RCrmHistory.php"); + $this->__loadLib("classes/retailcrm/RCrmHttpClient.php"); + $this->__loadLib("classes/retailcrm/RCrmIcml.php"); + $this->__loadLib("classes/retailcrm/RCrmProxy.php"); + + // Exceptions + $this->__loadLib("classes/retailcrm/RCrmCurlException.php"); + $this->__loadLib("classes/retailcrm/RCrmJsonException.php"); + + // Helpers + $this->__loadLib("classes/retailcrm/RCrmHelpers.php"); + $this->__implement("RCrmHelpers"); + + // Events + $this->__loadLib("__events.php"); + $this->__implement("__RetailCRM_events"); + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmActions.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmActions.php new file mode 100644 index 0000000..1b7f6f5 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmActions.php @@ -0,0 +1,265 @@ +getVal('//modules/RetailCRM/IgnoreObjectUpdateEvent/' . $order->getObject()->getId()); + if ($time == $order->getObject()->getUpdateTime() OR $time + 1 == $order->getObject()->getUpdateTime()) { + return; + } + + $config = mainConfiguration::getInstance(); + + $umiOrderStatusCode = order::getCodeByStatus($order->getOrderStatus()); + + $retailcrm = new RetailCRM; + + $relationMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderStatusMap')); + $crmOrderStatusCode = $retailcrm->getRelationByMap($relationMap, $umiOrderStatusCode); + + if (!$crmOrderStatusCode) { + return; + } + + $umiOrderPaymentType = $order->getObject()->getValue('payment_id'); + $relationOrderPaymentTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentTypeMap')); + $crmOrderPaymentType = $retailcrm->getRelationByMap($relationOrderPaymentTypesMap, $umiOrderPaymentType); + + $umiOrderPaymentStatus = $order->getObject()->getValue('payment_status_id'); + $relationOrderPaymentStatusesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentStatusMap')); + $crmOrderPaymentStatus = $retailcrm->getRelationByMap($relationOrderPaymentStatusesMap, $umiOrderPaymentStatus); + + $umiOrderDeliveryId = $order->getObject()->getValue('delivery_id'); + $relationOrderDeliveryTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderDeliveryTypeMap')); + $crmOrderDeliveryType = $retailcrm->getRelationByMap($relationOrderDeliveryTypesMap, $umiOrderDeliveryId); + + $customer = customer::get($order->getCustomerId()); + $orderItems = $order->getItems(); + + $orderItemsToCrm = array(); + + foreach ($orderItems as $orderItem) { + /** @var optionedOrderItem $orderItem */ + + $itemProperties = array(); + foreach ($orderItem->getOptions() as $option) { + $option = new umiObject($option['option-id']); + $itemProperties[] = array( + 'name' => $option->getType()->getName(), + 'value' => $option->getName() + ); + } + + $optionGroups = $orderItem->getItemElement()->getObject()->getType()->getFieldsGroupByName('catalog_option_props')->getFields(); + $optionGuidesToGroups = array(); + foreach ($optionGroups as $optionGroup) { + /** @var umiField $optionGroup */ + $optionGuidesToGroups[$optionGroup->getGuideId()] = $optionGroup->getId(); + } + + $options = array(); + foreach ($orderItem->getOptions() as $option) { + $option = $objects->getObject($option['option-id']); + $options[] = $optionGuidesToGroups[$option->getTypeId()] . '_' . $option->getId(); + } + + $product = $orderItem->getItemElement(); + + if (!empty($options)) { + $productId = $product->getId() . '#' . implode('-', $options); + } else { + $productId = $product->getId(); + } + + $orderItemsToCrm[] = array( + 'initialPrice' => $orderItem->getItemPrice(), + 'discount' => $orderItem->getDiscount(), + 'quantity' => $orderItem->getAmount(), + 'productName' => $product->getName(), + 'properties' => $itemProperties, + 'offer' => array( + 'externalId' => $productId + ) + ); + } + + /* One click order */ + if ($order->getObject()->getValue('purchaser_one_click') !== null) { + $oneClickObj = new umiObject($order->getObject()->getValue('purchaser_one_click')); + + $orderToCrm = array( + 'number' => $order->getObject()->getName(), + 'externalId' => $order->getId(), + 'phone' => $oneClickObj->getValue('phone'), + 'customer' => array( + 'externalId' => $customer->getId() + ), + 'paymentType' => $crmOrderPaymentType, + 'paymentStatus' => $crmOrderPaymentStatus, + 'status' => $crmOrderStatusCode, + 'items' => $orderItemsToCrm, + 'orderMethod' => 'one-click' + ); + } else { + if ($order->getObject()->getValue('delivery_address') !== null) { + $deliveryObjId = $order->getObject()->getValue('delivery_address'); + $deliveryObj = new umiObject($deliveryObjId); + + if ($deliveryObj->getValue('country') !== false) { + $deliveryCountryObjId = $deliveryObj->getValue('country'); + $deliveryCountryObj = new umiObject($deliveryCountryObjId); + $deliveryCountryIsoCode = $deliveryCountryObj->getValue('country_iso_code'); + } else { + $deliveryCountryIsoCode = ''; + } + + $deliveryIndex = $deliveryObj->getValue('index'); + $deliveryStreet = $deliveryObj->getValue('street'); + $deliveryBuilding = $deliveryObj->getValue('house'); + $deliveryHouse = $deliveryObj->getValue('house'); + $deliveryFlat = $deliveryObj->getValue('flat'); + $deliveryNotes = $deliveryObj->getValue('order_comments'); + + if ($deliveryObj->getValue('region') !== null) { + $deliveryRegionObj = new umiObject($deliveryObj->getValue('region')); + $deliveryRegion = $deliveryRegionObj->getName(); + } else { + $deliveryRegion = ''; + } + + if ($deliveryObj->getValue('city') !== null) { + $deliveryCityObj = new umiObject($deliveryObj->getValue('city')); + $deliveryCity = $deliveryCityObj->getName(); + } else { + $deliveryCity = ''; + } + + $addressToCrm = array( + 'countryIso' => $deliveryCountryIsoCode, + 'index' => $deliveryIndex, + 'region' => $deliveryRegion, + 'city' => $deliveryCity, + 'street' => $deliveryStreet, + 'building' => $deliveryBuilding, + 'flat' => $deliveryFlat, + 'house' => $deliveryHouse, + 'notes' => $deliveryNotes + ); + + } else { + $addressToCrm = array(); + } + + $orderToCrm = array( + 'number' => $order->getObject()->getName(), + 'externalId' => $order->getId(), + 'lastName' => $customer->getValue('lname'), + 'firstName' => $customer->getValue('fname'), + 'patronymic' => $customer->getValue('father_name'), + 'phone' => $customer->getValue('phone'), + 'email' => $customer->getValue('email'), + 'customer' => array( + 'externalId' => $customer->getId() + ), + 'paymentType' => $crmOrderPaymentType, + 'paymentStatus' => $crmOrderPaymentStatus, + 'status' => $crmOrderStatusCode, + 'items' => $orderItemsToCrm, + 'delivery' => array( + 'address' => $addressToCrm, + 'code' => $crmOrderDeliveryType + ) + ); + } + + // TODO: есть возможность учитывать домен + $api = new RCrmProxy( + $config->get('retailcrm', 'crmUrl'), + $config->get('retailcrm', 'apiKey'), + __DIR__ . '/../../../retailcrm.error.log' + ); + + if ($mode == 'create') { + $orderToCrm = self::customerPrepare($orderToCrm); + $api->ordersCreate($orderToCrm); + } else if ($mode == 'edit') { + $api->ordersEdit($orderToCrm); + } + } + + public static function customerPrepare($orderToCrm) { + $config = mainConfiguration::getInstance(); + + $api = new RCrmProxy( + $config->get('retailcrm', 'crmUrl'), + $config->get('retailcrm', 'apiKey'), + __DIR__ . '/../../../retailcrm.error.log' + ); + + $crmCustomer = $api->customersGet($orderToCrm['customer']['externalId']); + + if (!$crmCustomer) { + $crmCustomers = $api->customersList(array( + 'name' => $orderToCrm['phone'], + 'email' => $orderToCrm['email'] + )); + + $foundedCustomerExternalId = false; + if ($crmCustomers) { + /** @var RCrmApiResponse $crmCustomers */ + $crmCustomers = $crmCustomers->getCustomers(); + + if (count($crmCustomers) > 0) { + foreach ($crmCustomers as $crmCustomer) { + if (isset($crmCustomer['externalId']) && $crmCustomer['externalId'] > 0) { + $foundedCustomerExternalId = true; + $orderToCrm['customer']['externalId'] = $crmCustomer['externalId']; + break; + } + } + + if (!$foundedCustomerExternalId) { + $crmCustomer = $crmCustomers[0]; + $status = $api->customersFixExternalIds(array( + 'id' => $crmCustomer['id'], + 'externalId' => $crmCustomer['externalId'] + )); + + if (!$status) { + unset($orderToCrm['customer']); + } + } + } else { + $status = $api->customersCreate(array( + 'externalId' => $orderToCrm['customer']['externalId'], + 'firstName' => $orderToCrm['firstName'], + 'lastName' => $orderToCrm['lastName'], + 'patronymic' => $orderToCrm['patronymic'], + 'email' => $orderToCrm['email'], + 'phones' => array( + 'number' => $orderToCrm['phone'] + ) + )); + + if (!$status) { + unset($orderToCrm['customer']); + } + } + } else { + unset($orderToCrm['customer']); + } + } + + return $orderToCrm; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmApiClient.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmApiClient.php new file mode 100644 index 0000000..d35de61 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmApiClient.php @@ -0,0 +1,1825 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * @link http://www.retailcrm.ru/docs/Developers/ApiVersion4 + */ +class RCrmApiClient +{ + + const VERSION = 'v4'; + + protected $client; + + /** + * Site code + */ + protected $siteCode; + + /** + * Client creating + * + * @param string $url api url + * @param string $apiKey api key + * @param string $site site code + * + * @throws InvalidArgumentException + */ + public function __construct($url, $apiKey, $site = null) + { + if ('/' !== $url[strlen($url) - 1]) { + $url .= '/'; + } + + $url = $url . 'api/' . self::VERSION; + + $this->client = new RCrmHttpClient($url, array('apiKey' => $apiKey)); + $this->siteCode = $site; + } + + /** + * Returns users list + * + * @param array $filter + * @param null $page + * @param null $limit + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function usersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/users', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Get user groups + * + * @param null $page + * @param null $limit + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * + * @return RCrmApiResponse + */ + public function usersGroups($page = null, $limit = null) + { + $parameters = array(); + + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/user-groups', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Returns user data + * + * @param integer $id user ID + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function usersGet($id) + { + return $this->client->makeRequest("/users/$id", RCrmHttpClient::METHOD_GET); + } + + /** + * Returns filtered orders list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/orders', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Create a order + * + * @param array $order order data + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersCreate(array $order, $site = null) + { + if (!count($order)) { + throw new InvalidArgumentException( + 'Parameter `order` must contains a data' + ); + } + + return $this->client->makeRequest( + '/orders/create', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('order' => json_encode($order))) + ); + } + + /** + * Save order IDs' (id and externalId) association in the CRM + * + * @param array $ids order identificators + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersFixExternalIds(array $ids) + { + if (! count($ids)) { + throw new InvalidArgumentException( + 'Method parameter must contains at least one IDs pair' + ); + } + + return $this->client->makeRequest( + '/orders/fix-external-ids', + RCrmHttpClient::METHOD_POST, + array('orders' => json_encode($ids) + ) + ); + } + + /** + * Returns statuses of the orders + * + * @param array $ids (default: array()) + * @param array $externalIds (default: array()) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersStatuses(array $ids = array(), array $externalIds = array()) + { + $parameters = array(); + + if (count($ids)) { + $parameters['ids'] = $ids; + } + if (count($externalIds)) { + $parameters['externalIds'] = $externalIds; + } + + return $this->client->makeRequest( + '/orders/statuses', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Upload array of the orders + * + * @param array $orders array of orders + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersUpload(array $orders, $site = null) + { + if (!count($orders)) { + throw new InvalidArgumentException( + 'Parameter `orders` must contains array of the orders' + ); + } + + return $this->client->makeRequest( + '/orders/upload', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('orders' => json_encode($orders))) + ); + } + + /** + * Get order by id or externalId + * + * @param string $id order identificator + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest( + "/orders/$id", + RCrmHttpClient::METHOD_GET, + $this->fillSite($site, array('by' => $by)) + ); + } + + /** + * Edit a order + * + * @param array $order order data + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersEdit(array $order, $by = 'externalId', $site = null) + { + if (!count($order)) { + throw new InvalidArgumentException( + 'Parameter `order` must contains a data' + ); + } + + $this->checkIdParameter($by); + + if (!array_key_exists($by, $order)) { + throw new InvalidArgumentException( + sprintf('Order array must contain the "%s" parameter.', $by) + ); + } + + return $this->client->makeRequest( + sprintf('/orders/%s/edit', $order[$by]), + RCrmHttpClient::METHOD_POST, + $this->fillSite( + $site, + array('order' => json_encode($order), 'by' => $by) + ) + ); + } + + /** + * Get orders history + * @param array $filter + * @param null $page + * @param null $limit + * + * @return RCrmApiResponse + */ + public function ordersHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/orders/history', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Returns filtered customers list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/customers', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Create a customer + * + * @param array $customer customer data + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersCreate(array $customer, $site = null) + { + if (! count($customer)) { + throw new InvalidArgumentException( + 'Parameter `customer` must contains a data' + ); + } + + return $this->client->makeRequest( + '/customers/create', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('customer' => json_encode($customer))) + ); + } + + /** + * Save customer IDs' (id and externalId) association in the CRM + * + * @param array $ids ids mapping + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersFixExternalIds(array $ids) + { + if (! count($ids)) { + throw new InvalidArgumentException( + 'Method parameter must contains at least one IDs pair' + ); + } + + return $this->client->makeRequest( + '/customers/fix-external-ids', + RCrmHttpClient::METHOD_POST, + array('customers' => json_encode($ids)) + ); + } + + /** + * Upload array of the customers + * + * @param array $customers array of customers + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersUpload(array $customers, $site = null) + { + if (! count($customers)) { + throw new InvalidArgumentException( + 'Parameter `customers` must contains array of the customers' + ); + } + + return $this->client->makeRequest( + '/customers/upload', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('customers' => json_encode($customers))) + ); + } + + /** + * Get customer by id or externalId + * + * @param string $id customer identificator + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest( + "/customers/$id", + RCrmHttpClient::METHOD_GET, + $this->fillSite($site, array('by' => $by)) + ); + } + + /** + * Edit a customer + * + * @param array $customer customer data + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function customersEdit(array $customer, $by = 'externalId', $site = null) + { + if (!count($customer)) { + throw new InvalidArgumentException( + 'Parameter `customer` must contains a data' + ); + } + + $this->checkIdParameter($by); + + if (!array_key_exists($by, $customer)) { + throw new InvalidArgumentException( + sprintf('Customer array must contain the "%s" parameter.', $by) + ); + } + + return $this->client->makeRequest( + sprintf('/customers/%s/edit', $customer[$by]), + RCrmHttpClient::METHOD_POST, + $this->fillSite( + $site, + array('customer' => json_encode($customer), 'by' => $by) + ) + ); + } + + /** + * Get customers history + * @param array $filter + * @param null $page + * @param null $limit + * + * @return RCrmApiResponse + */ + public function customersHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/customers/history', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Get orders assembly list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/orders/packs', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Create orders assembly + * + * @param array $pack pack data + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksCreate(array $pack, $site = null) + { + if (!count($pack)) { + throw new InvalidArgumentException( + 'Parameter `pack` must contains a data' + ); + } + + return $this->client->makeRequest( + '/orders/packs/create', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('pack' => json_encode($pack))) + ); + } + + /** + * Get orders assembly history + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/orders/packs/history', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Get orders assembly by id + * + * @param string $id pack identificator + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksGet($id) + { + if (empty($id)) { + throw new InvalidArgumentException('Parameter `id` must be set'); + } + + return $this->client->makeRequest( + "/orders/packs/$id", + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Delete orders assembly by id + * + * @param string $id pack identificator + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksDelete($id) + { + if (empty($id)) { + throw new InvalidArgumentException('Parameter `id` must be set'); + } + + return $this->client->makeRequest( + sprintf('/orders/packs/%s/delete', $id), + RCrmHttpClient::METHOD_POST + ); + } + + /** + * Edit orders assembly + * + * @param array $pack pack data + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function ordersPacksEdit(array $pack, $site = null) + { + if (!count($pack) || empty($pack['id'])) { + throw new InvalidArgumentException( + 'Parameter `pack` must contains a data & pack `id` must be set' + ); + } + + return $this->client->makeRequest( + sprintf('/orders/packs/%s/edit', $pack['id']), + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('pack' => json_encode($pack))) + ); + } + + /** + * Get purchace prices & stock balance + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storeInventories(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/store/inventories', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Get store settings + * + * @param string $code get settings code + * + * @return RCrmApiResponse + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function storeSettingsGet($code) + { + if (empty($code)) { + throw new InvalidArgumentException('Parameter `code` must be set'); + } + + return $this->client->makeRequest( + "/store/setting/$code", + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit store configuration + * + * @param array $configuration + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function storeSettingsEdit(array $configuration) + { + if (!count($configuration) || empty($configuration['code'])) { + throw new InvalidArgumentException( + 'Parameter `configuration` must contains a data & configuration `code` must be set' + ); + } + + return $this->client->makeRequest( + sprintf('/store/setting/%s/edit', $configuration['code']), + RCrmHttpClient::METHOD_POST, + array('configuration' => json_encode($configuration)) + ); + } + + /** + * Upload store inventories + * + * @param array $offers offers data + * @param string $site (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storeInventoriesUpload(array $offers, $site = null) + { + if (!count($offers)) { + throw new InvalidArgumentException( + 'Parameter `offers` must contains array of the offers' + ); + } + + return $this->client->makeRequest( + '/store/inventories/upload', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('offers' => json_encode($offers))) + ); + } + + /** + * Upload store prices + * + * @param array $prices prices data + * @param string $site default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storePricesUpload(array $prices, $site = null) + { + if (!count($prices)) { + throw new InvalidArgumentException( + 'Parameter `prices` must contains array of the prices' + ); + } + + return $this->client->makeRequest( + '/store/prices/upload', + RCrmHttpClient::METHOD_POST, + $this->fillSite($site, array('prices' => json_encode($prices))) + ); + } + + /** + * Get products + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storeProducts(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (count($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest( + '/store/products', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Get delivery settings + * + * @param string $code + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function deliverySettingsGet($code) + { + if (empty($code)) { + throw new InvalidArgumentException('Parameter `code` must be set'); + } + + return $this->client->makeRequest( + "/delivery/generic/setting/$code", + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit delivery configuration + * + * @param array $configuration + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function deliverySettingsEdit(array $configuration) + { + if (!count($configuration) || empty($configuration['code'])) { + throw new InvalidArgumentException( + 'Parameter `configuration` must contains a data & configuration `code` must be set' + ); + } + + return $this->client->makeRequest( + sprintf('/delivery/generic/setting/%s/edit', $configuration['code']), + RCrmHttpClient::METHOD_POST, + array('configuration' => json_encode($configuration)) + ); + } + + /** + * Delivery tracking update + * + * @param string $code + * @param array $statusUpdate + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function deliveryTracking($code, array $statusUpdate) + { + if (empty($code)) { + throw new InvalidArgumentException('Parameter `code` must be set'); + } + + if (!count($statusUpdate)) { + throw new InvalidArgumentException( + 'Parameter `statusUpdate` must contains a data' + ); + } + + return $this->client->makeRequest( + sprintf('/delivery/generic/%s/tracking', $code), + RCrmHttpClient::METHOD_POST, + $statusUpdate + ); + } + + /** + * Returns available county list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function countriesList() + { + return $this->client->makeRequest( + '/reference/countries', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Returns deliveryServices list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function deliveryServicesList() + { + return $this->client->makeRequest( + '/reference/delivery-services', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit deliveryService + * + * @param array $data delivery service data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function deliveryServicesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/delivery-services/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('deliveryService' => json_encode($data)) + ); + } + + /** + * Returns deliveryTypes list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function deliveryTypesList() + { + return $this->client->makeRequest( + '/reference/delivery-types', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit deliveryType + * + * @param array $data delivery type data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function deliveryTypesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/delivery-types/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('deliveryType' => json_encode($data)) + ); + } + + /** + * Returns orderMethods list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function orderMethodsList() + { + return $this->client->makeRequest( + '/reference/order-methods', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit orderMethod + * + * @param array $data order method data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function orderMethodsEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/order-methods/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('orderMethod' => json_encode($data)) + ); + } + + /** + * Returns orderTypes list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function orderTypesList() + { + return $this->client->makeRequest( + '/reference/order-types', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit orderType + * + * @param array $data order type data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function orderTypesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/order-types/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('orderType' => json_encode($data)) + ); + } + + /** + * Returns paymentStatuses list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function paymentStatusesList() + { + return $this->client->makeRequest( + '/reference/payment-statuses', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit paymentStatus + * + * @param array $data payment status data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function paymentStatusesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/payment-statuses/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('paymentStatus' => json_encode($data)) + ); + } + + /** + * Returns paymentTypes list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function paymentTypesList() + { + return $this->client->makeRequest( + '/reference/payment-types', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit paymentType + * + * @param array $data payment type data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function paymentTypesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/payment-types/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('paymentType' => json_encode($data)) + ); + } + + /** + * Returns productStatuses list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function productStatusesList() + { + return $this->client->makeRequest( + '/reference/product-statuses', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit productStatus + * + * @param array $data product status data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function productStatusesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/product-statuses/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('productStatus' => json_encode($data)) + ); + } + + /** + * Returns sites list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function sitesList() + { + return $this->client->makeRequest( + '/reference/sites', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit site + * + * @param array $data site data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function sitesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/sites/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('site' => json_encode($data)) + ); + } + + /** + * Returns statusGroups list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function statusGroupsList() + { + return $this->client->makeRequest( + '/reference/status-groups', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Returns statuses list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function statusesList() + { + return $this->client->makeRequest( + '/reference/statuses', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit order status + * + * @param array $data status data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function statusesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/statuses/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('status' => json_encode($data)) + ); + } + + /** + * Returns stores list + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storesList() + { + return $this->client->makeRequest( + '/reference/stores', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit store + * + * @param array $data site data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function storesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + if (!array_key_exists('name', $data)) { + throw new InvalidArgumentException( + 'Data must contain "name" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/stores/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('store' => json_encode($data)) + ); + } + + /** + * Get prices types + * + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function pricesTypes() + { + return $this->client->makeRequest( + '/reference/price-types', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit price type + * + * @param array $data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function pricesEdit(array $data) + { + if (!array_key_exists('code', $data)) { + throw new InvalidArgumentException( + 'Data must contain "code" parameter.' + ); + } + + if (!array_key_exists('name', $data)) { + throw new InvalidArgumentException( + 'Data must contain "name" parameter.' + ); + } + + return $this->client->makeRequest( + sprintf('/reference/price-types/%s/edit', $data['code']), + RCrmHttpClient::METHOD_POST, + array('priceType' => json_encode($data)) + ); + } + + /** + * Get telephony settings + * + * @param string $code + * + * @throws RCrmJsonException + * @throws RCrmCurlException + * @throws InvalidArgumentException + * + * @return RCrmApiResponse + */ + public function telephonySettingsGet($code) + { + if (empty($code)) { + throw new InvalidArgumentException('Parameter `code` must be set'); + } + + return $this->client->makeRequest( + "/telephony/setting/$code", + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Edit telephony settings + * + * @param string $code symbolic code + * @param string $clientId client id + * @param boolean $active telephony activity + * @param mixed $name service name + * @param mixed $makeCallUrl service init url + * @param mixed $image service logo url(svg file) + * + * @param array $additionalCodes + * @param array $externalPhones + * @param bool $allowEdit + * @param bool $inputEventSupported + * @param bool $outputEventSupported + * @param bool $hangupEventSupported + * @param bool $changeUserStatusUrl + * + * @return RCrmApiResponse + */ + public function telephonySettingsEdit( + $code, + $clientId, + $active = false, + $name = false, + $makeCallUrl = false, + $image = false, + $additionalCodes = array(), + $externalPhones = array(), + $allowEdit = false, + $inputEventSupported = false, + $outputEventSupported = false, + $hangupEventSupported = false, + $changeUserStatusUrl = false + ) + { + if (!isset($code)) { + throw new InvalidArgumentException('Code must be set'); + } + + $parameters['code'] = $code; + + if (!isset($clientId)) { + throw new InvalidArgumentException('client id must be set'); + } + + $parameters['clientId'] = $clientId; + + if (!isset($active)) { + $parameters['active'] = false; + } else { + $parameters['active'] = $active; + } + + if (!isset($name)) { + throw new InvalidArgumentException('name must be set'); + } + + if (isset($name)) { + $parameters['name'] = $name; + } + + if (isset($makeCallUrl)) { + $parameters['makeCallUrl'] = $makeCallUrl; + } + + if (isset($image)) { + $parameters['image'] = $image; + } + + if (isset($additionalCodes)) { + $parameters['additionalCodes'] = $additionalCodes; + } + + if (isset($externalPhones)) { + $parameters['externalPhones'] = $externalPhones; + } + + if (isset($allowEdit)) { + $parameters['allowEdit'] = $allowEdit; + } + + if (isset($inputEventSupported)) { + $parameters['inputEventSupported'] = $inputEventSupported; + } + + if (isset($outputEventSupported)) { + $parameters['outputEventSupported'] = $outputEventSupported; + } + + if (isset($hangupEventSupported)) { + $parameters['hangupEventSupported'] = $hangupEventSupported; + } + + if (isset($changeUserStatusUrl)) { + $parameters['changeUserStatusUrl'] = $changeUserStatusUrl; + } + + return $this->client->makeRequest( + "/telephony/setting/$code/edit", + RCrmHttpClient::METHOD_POST, + array('configuration' => json_encode($parameters)) + ); + } + + /** + * Call event + * + * @param string $phone phone number + * @param string $type call type + * @param array $codes + * @param string $hangupStatus + * @param string $externalPhone + * @param array $webAnalyticsData + * + * @return RCrmApiResponse + * @internal param string $code additional phone code + * @internal param string $status call status + * + */ + public function telephonyCallEvent( + $phone, + $type, + $codes, + $hangupStatus, + $externalPhone = null, + $webAnalyticsData = array() + ) + { + if (!isset($phone)) { + throw new InvalidArgumentException('Phone number must be set'); + } + + if (!isset($type)) { + throw new InvalidArgumentException('Type must be set (in|out|hangup)'); + } + + if (empty($codes)) { + throw new InvalidArgumentException('Codes array must be set'); + } + + $parameters['phone'] = $phone; + $parameters['type'] = $type; + $parameters['codes'] = $codes; + $parameters['hangupStatus'] = $hangupStatus; + $parameters['callExternalId'] = $externalPhone; + $parameters['webAnalyticsData'] = $webAnalyticsData; + + return $this->client->makeRequest( + '/telephony/call/event', + RCrmHttpClient::METHOD_POST, + array('event' => json_encode($parameters)) + ); + } + + /** + * Upload calls + * + * @param array $calls calls data + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function telephonyCallsUpload(array $calls) + { + if (!count($calls)) { + throw new InvalidArgumentException( + 'Parameter `calls` must contains array of the calls' + ); + } + + return $this->client->makeRequest( + '/telephony/calls/upload', + RCrmHttpClient::METHOD_POST, + array('calls' => json_encode($calls)) + ); + } + + /** + * Get call manager + * + * @param string $phone phone number + * @param bool $details detailed information + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function telephonyCallManager($phone, $details) + { + if (!isset($phone)) { + throw new InvalidArgumentException('Phone number must be set'); + } + + $parameters['phone'] = $phone; + $parameters['details'] = isset($details) ? $details : 0; + + return $this->client->makeRequest( + '/telephony/manager', + RCrmHttpClient::METHOD_GET, + $parameters + ); + } + + /** + * Update CRM basic statistic + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function statisticUpdate() + { + return $this->client->makeRequest( + '/statistic/update', + RCrmHttpClient::METHOD_GET + ); + } + + /** + * Return current site + * + * @return string + */ + public function getSite() + { + return $this->siteCode; + } + + /** + * Set site + * + * @param string $site site code + * + * @return void + */ + public function setSite($site) + { + $this->siteCode = $site; + } + + /** + * Check ID parameter + * + * @param string $by identify by + * + * @throws InvalidArgumentException + * + * @return bool + */ + protected function checkIdParameter($by) + { + $allowedForBy = array( + 'externalId', + 'id' + ); + + if (!in_array($by, $allowedForBy, false)) { + throw new InvalidArgumentException( + sprintf( + 'Value "%s" for "by" param is not valid. Allowed values are %s.', + $by, + implode(', ', $allowedForBy) + ) + ); + } + + return true; + } + + /** + * Fill params by site value + * + * @param string $site site code + * @param array $params input parameters + * + * @return array + */ + protected function fillSite($site, array $params) + { + if ($site) { + $params['site'] = $site; + } elseif ($this->siteCode) { + $params['site'] = $this->siteCode; + } + + return $params; + } +} diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmApiResponse.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmApiResponse.php new file mode 100644 index 0000000..aaa5580 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmApiResponse.php @@ -0,0 +1,164 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * @link http://www.retailcrm.ru/docs/Developers/ApiVersion4 + */ +class RCrmApiResponse implements ArrayAccess +{ + // HTTP response status code + protected $statusCode; + + // response assoc array + protected $response; + + /** + * ApiResponse constructor. + * + * @param int $statusCode HTTP status code + * @param mixed $responseBody HTTP body + * + * @throws RCrmJsonException + */ + public function __construct($statusCode, $responseBody = null) + { + $this->statusCode = (int)$statusCode; + + if (!empty($responseBody)) { + $response = json_decode($responseBody, true); + + if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { + throw new RCrmJsonException( + "Invalid JSON in the API response body. Error code #$error", + $error + ); + } + + $this->response = $response; + } + } + + /** + * Return HTTP response status code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * HTTP request was successful + * + * @return bool + */ + public function isSuccessful() + { + return $this->statusCode < 400; + } + + /** + * Allow to access for the property throw class method + * + * @param string $name method name + * @param mixed $arguments method parameters + * + * @throws InvalidArgumentException + * + * @return mixed + */ + public function __call($name, $arguments) + { + // convert getSomeProperty to someProperty + $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); + + if (!isset($this->response[$propertyName])) { + throw new InvalidArgumentException("Method \"$name\" not found"); + } + + return $this->response[$propertyName]; + } + + /** + * Allow to access for the property throw object property + * + * @param string $name property name + * + * @throws InvalidArgumentException + * + * @return mixed + */ + public function __get($name) + { + if (!isset($this->response[$name])) { + throw new InvalidArgumentException("Property \"$name\" not found"); + } + + return $this->response[$name]; + } + + /** + * Offset set + * + * @param mixed $offset offset + * @param mixed $value value + * + * @throws BadMethodCallException + * @return void + */ + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('This activity not allowed'); + } + + /** + * Offset unset + * + * @param mixed $offset offset + * + * @throws BadMethodCallException + * @return void + */ + public function offsetUnset($offset) + { + throw new BadMethodCallException('This call not allowed'); + } + + /** + * Check offset + * + * @param mixed $offset offset + * + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->response[$offset]); + } + + /** + * Get offset + * + * @param mixed $offset offset + * + * @throws InvalidArgumentException + * + * @return mixed + */ + public function offsetGet($offset) + { + if (!isset($this->response[$offset])) { + throw new InvalidArgumentException("Property \"$offset\" not found"); + } + + return $this->response[$offset]; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmCurlException.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmCurlException.php new file mode 100644 index 0000000..0fe29a8 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmCurlException.php @@ -0,0 +1,5 @@ + ', $mapItem); + $map[$mapItem[0]] = $mapItem[1]; + } + + return $map; + } + + /** + * @param $map array + * @param $item string + * @param $reversed bool + * @return string|null + */ + public function getRelationByMap($map, $item, $reversed = false) + { + if (!$reversed) { + if (isset($map[$item]) && !empty($map[$item])) { + return $map[$item]; + } else { + return null; + } + } else { + foreach ($map as $umiStatusOrder => $crmStatusOrder) { + if ($crmStatusOrder == $item) { + return $umiStatusOrder; + } + } + return null; + } + } + + /** + * @param $orderHistory array + * @return array + */ + public function getAssemblyOrder($orderHistory) + { + if (file_exists(__DIR__ . '/../../data/objects.xml')) { + $objects = simplexml_load_file(__DIR__ . '/../../data/objects.xml'); + foreach ($objects->fields->field as $object) { + $fields[(string)$object["group"]][(string)$object["id"]] = (string)$object; + } + } + + $orders = array(); + + foreach ($orderHistory as $change) { + $change['order'] = $this->removeEmpty($change['order']); + + $orderId = $change['order']['id']; + + if (isset($change['order']['items'])) { + $items = array(); + foreach ($change['order']['items'] as $item) { + if (isset($change['created'])) { + $item['created'] = 1; + } + $items[$item['id']] = $item; + } + $change['order']['items'] = $items; + } + + if (isset($change['order']['contragent']['contragentType'])) { + $change['order']['contragentType'] = $change['order']['contragent']['contragentType']; + unset($change['order']['contragent']); + } + + if (isset($orders[$orderId])) { + $orders[$orderId] = array_merge($orders[$orderId], $change['order']); + } else { + $orders[$orderId] = $change['order']; + } + + if (isset($change['item'])) { + $itemId = $change['item']['id']; + + if ($orders[$orderId]['items'][$itemId]) { + $orders[$orderId]['items'][$itemId] = array_merge($orders[$orderId]['items'][$itemId], + $change['item']); + } else { + $orders[$orderId]['items'][$itemId] = $change['item']; + } + + if (empty($change['oldValue']) && $change['field'] == 'order_product') { + $orders[$orderId]['items'][$itemId]['created'] = true; + } + + if (empty($change['newValue']) && $change['field'] == 'order_product') { + $orders[$orderId]['items'][$itemId]['deleted'] = true; + } + + if (!$orders[$orderId]['items'][$itemId]['created'] && $fields['item'][$change['field']]) { + $orders[$orderId]['items'][$itemId][$fields['item'][$change['field']]] = $change['newValue']; + } + } else { + if (isset($fields['delivery'][$change['field']]) && $fields['delivery'][$change['field']] == 'service') { + $orders[$orderId]['delivery']['service']['code'] = $this->historyNewValue($change['newValue']); + } elseif (isset($fields['delivery'][$change['field']])) { + $orders[$orderId]['delivery'][$fields['delivery'][$change['field']]] = $this->historyNewValue($change['newValue']); + } elseif (isset($fields['orderAddress'][$change['field']])) { + $orders[$orderId]['delivery']['address'][$fields['orderAddress'][$change['field']]] = $change['newValue']; + } elseif (isset($fields['integrationDelivery'][$change['field']])) { + $orders[$orderId]['delivery']['service'][$fields['integrationDelivery'][$change['field']]] = $this->historyNewValue($change['newValue']); + } elseif (isset($fields['customerContragent'][$change['field']])) { + $orders[$orderId][$fields['customerContragent'][$change['field']]] = $this->historyNewValue($change['newValue']); + } elseif (strripos($change['field'], 'custom_') !== false) { + $orders[$orderId]['customFields'][str_replace('custom_', '', $change['field'])] = $this->historyNewValue($change['newValue']); + } elseif (isset($fields['order'][$change['field']])) { + $orders[$orderId][$fields['order'][$change['field']]] = $this->historyNewValue($change['newValue']); + } + + if (isset($change['created'])) { + $orders[$orderId]['created'] = 1; + } + + if (isset($change['deleted'])) { + $orders[$orderId]['deleted'] = 1; + } + } + } + + return array_values($orders); + } + + /** + * @param $value mixed + * @return string + */ + public function historyNewValue($value) + { + if (isset($value['code'])) { + return $value['code']; + } else { + return $value; + } + } + + /** + * @param $inputArray mixed + * @return array + */ + public function removeEmpty($inputArray) + { + $outputArray = array(); + if (!empty($inputArray)) { + foreach ($inputArray as $key => $element) { + if (!empty($element) || $element === 0 || $element === '0') { + if (is_array($element)) { + $element = $this->removeEmpty($element); + } + $outputArray[$key] = $element; + } + } + } + + return $outputArray; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmHistory.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmHistory.php new file mode 100644 index 0000000..23bcdce --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmHistory.php @@ -0,0 +1,470 @@ +api = new RCrmProxy( + $config->get('retailcrm', 'crmUrl'), + $config->get('retailcrm', 'apiKey'), + __DIR__ . '/../../../retailcrm.error.log' + ); + } + + public function runOrders() + { + $retailcrm = new RetailCRM; + $regedit = regedit::getInstance(); + $config = mainConfiguration::getInstance(); + + $historyLastId = $config->get('retailcrm', 'lastHistorySinceId'); + + if (!$historyLastId) { + $historyLastId = 0; + } + + $historyPage = 1; + $historyArray = array(); + + do { + $response = $this->api->ordersHistory(array('sinceId' => $historyLastId), $historyPage); + $historyPage++; + + if (!is_null($response) && count($response['history'])) { + $historyArray = array_merge($historyArray, $response['history']); + } else { + break; + } + } while ($response['pagination']['currentPage'] != $response['pagination']['totalPageCount']); + + if (count($historyArray)) { + $lastChange = end($historyArray); + + $crmOrders = $retailcrm->getAssemblyOrder($historyArray); + + $objectTypes = umiObjectTypesCollection::getInstance(); + $objects = umiObjectsCollection::getInstance(); + + foreach ($crmOrders as $crmOrder) { + $order = null; + + if (isset($crmOrder['externalId'])) { + $order = order::get($crmOrder['externalId']); + } + + if ((!isset($crmOrder['externalId']) || !$order) && isset($crmOrder['created'])) { + if (isset($crmOrder['deleted']) && $crmOrder['deleted']) { + continue; + } + + $order = order::create(); + + /* Order create date */ + $order->getObject()->getPropByName('order_date')->setValue(umiDate::getTimeStamp($crmOrder['createdAt'])); + + $crmCustomer = $crmOrder['customer']; + if (isset($crmCustomer['externalId']) && $crmCustomer['externalId'] > 0) { + // TODO: проверить существует ли такой пользователь в системе, если нет, то создать, т.к. принимая customer externalId мы считаем, что такой пользователь уже есть + $order->getObject()->getPropByName('customer_id')->setValue($crmCustomer['externalId']); + } else { + $customer = $objects->getObjectByName($crmCustomer['id'] . '-retailcrm'); + + if (!$customer) { + $customerObjectTypeId = $objectTypes->getBaseType('emarket', 'customer'); + + $customerId = $objects->addObject( + $crmCustomer['id'] . '-retailcrm', + $customerObjectTypeId + ); + + $customer = $objects->getObject($customerId); + $customer->setOwnerId($objects->getObjectIdByGUID('system-guest')); + $customer->commit(); + + $expirations = umiObjectsExpiration::getInstance(); + $expirations->add($customerId, customer::$defaultExpiration); + + if (!empty($crmCustomer['firstName'])) { + $customer->getPropByName('fname')->setValue($crmCustomer['firstName']); + } + + if (!empty($crmCustomer['lastName'])) { + $customer->getPropByName('lname')->setValue($crmCustomer['lastName']); + } + + if (!empty($crmCustomer['patronymic'])) { + $customer->getPropByName('father_name')->setValue($crmCustomer['patronymic']); + } + + if (!empty($crmCustomer['email'])) { + $customer->getPropByName('email')->setValue($crmCustomer['email']); + } + + if (isset($crmCustomer['phones']) && count($crmCustomer['phones']) > 0) { + $customer->getPropByName('phone')->setValue($crmCustomer['phones'][0]['number']); + } + + if (isset($crmCustomer['address'])) { + $deliveryTypeId = $objectTypes->getTypeIdByGUID('emarket-deliveryaddress'); + + $deliveryObjectId = $objects->addObject( + 'Address for customer ' . $customer->getId(), + $deliveryTypeId + ); + + $deliveryObject = $objects->getObject($deliveryObjectId); + + $crmCustomerAddress = $crmCustomer['address']; + + if (!empty($crmCustomerAddress['countryIso'])) { + $selector = new selector('objects'); + $selector->types('object-type')->id($config->get('retailcrm', 'countryGuideId')); + + $countries = $selector->result(); + + foreach ($countries as $country) { + /** @var umiObject $country */ + + $countryCode = $country->getValue('country_iso_code'); + + if ($crmCustomerAddress['countryIso'] == $countryCode) { + $deliveryObject->getPropByName('country')->setValue($country->getId()); + break; + } + } + } + if (!empty($crmCustomerAddress['index'])) { + $deliveryObject->getPropByName('index')->setValue($crmCustomerAddress['index']); + } + + if (!empty($crmCustomerAddress['region'])) { + $deliveryObject->getPropByName('region')->setValue($crmCustomerAddress['region']); + } + + if (!empty($crmCustomerAddress['city'])) { + $deliveryObject->getPropByName('city')->setValue($crmCustomerAddress['city']); + } + + if (!empty($crmCustomerAddress['street'])) { + $deliveryObject->getPropByName('street')->setValue($crmCustomerAddress['street']); + } + + if (!empty($crmCustomerAddress['building'])) { + $deliveryObject->getPropByName('house')->setValue($crmCustomerAddress['building']); + } + + if (!empty($crmCustomerAddress['flat'])) { + $deliveryObject->getPropByName('flat')->setValue($crmCustomerAddress['flat']); + } + + $deliveryAddresses = array( + 0 => $deliveryObject->getId() + ); + + $customer->getPropByName('delivery_addresses')->setValue($deliveryAddresses); + } + } + + $order->getObject()->getPropByName('customer_id')->setValue($customer->getId()); + } + + $orderItems = $order->getItems(); + foreach ($orderItems as $orderItem) { + $order->removeItem($orderItem); + } + + $crmItems = $crmOrder['items']; + foreach ($crmItems as $crmItem) { + if (isset($crmItem['deleted']) && $crmItem['deleted'] == true) { + continue; + } + if (!isset($crmItem['offer']['externalId'])) { + continue; + } + + if (mb_strpos($crmItem['offer']['externalId'], '#')) { + $data = explode('#', $crmItem['offer']['externalId']); + $itemId = $data[0]; + + $orderItem = orderItem::create($itemId); + + $itemOptions = $data[1]; + $itemOptions = explode('-', $itemOptions); + foreach ($itemOptions as $itemOption) { + $itemOption = explode('_', $itemOption); + $itemOptionGroupId = $itemOption[0]; + $itemOptionValue = $itemOption[1]; + $itemOptionObject = new umiField($itemOptionGroupId); + + $orderItem->appendOption($itemOptionObject->getName(), $itemOptionValue); + } + } else { + $orderItem = orderItem::create($crmItem['offer']['externalId']); + } + + /** @var optionedOrderItem $orderItem */ + $orderItem->setAmount($crmItem['quantity']); + + $order->appendItem($orderItem); + } + + if (isset($crmOrder['paymentType'])) { + $relationOrderPaymentTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentTypeMap')); + $umiOrderPaymentType = $retailcrm->getRelationByMap($relationOrderPaymentTypesMap, $crmOrder['paymentType'], true); + $order->getObject()->getPropByName('payment_id')->setValue($umiOrderPaymentType); + } + + if (isset($crmOrder['paymentStatus'])) { + $relationOrderPaymentStatusesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentStatusMap')); + $umiOrderPaymentStatus = $retailcrm->getRelationByMap($relationOrderPaymentStatusesMap, $crmOrder['paymentStatus'], true); + $order->getObject()->getPropByName('payment_status_id')->setValue($umiOrderPaymentStatus); + } + + if (isset($crmOrder['delivery']['code'])) { + $relationOrderDeliveryTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderDeliveryTypeMap')); + $umiOrderDeliveryType = $retailcrm->getRelationByMap($relationOrderDeliveryTypesMap, $crmOrder['delivery']['code'], true); + $order->getObject()->getPropByName('delivery_id')->setValue($umiOrderDeliveryType); + } + + if (isset($crmOrder['delivery']['address']) && count($crmOrder['delivery']['address'])) { + $crmDeliveryAddress = $crmOrder['delivery']['address']; + + $deliveryTypeId = $objectTypes->getTypeIdByGUID('emarket-deliveryaddress'); + $deliveryObjectId = $objects->addObject('Address for order ' . $order->getId(), $deliveryTypeId); + $deliveryObject = $objects->getObject($deliveryObjectId); + + if (!empty($crmDeliveryAddress['countryIso'])) { + $selector = new selector('objects'); + try { + $selector->types('object-type')->id($config->get('retailcrm', 'countryGuideId')); + $countries = $selector->result(); + + foreach ($countries as $country) { + /** @var umiObject $country */ + + $countryCode = $country->getValue('country_iso_code'); + + if ($crmDeliveryAddress['countryIso'] == $countryCode) { + $deliveryObject->getPropByName('country')->setValue($country->getId()); + break; + } + } + } catch (selectorException $e) {} + } + + if (!empty($crmDeliveryAddress['index'])) { + $deliveryObject->getPropByName('index')->setValue($crmDeliveryAddress['index']); + } + + if (!empty($crmDeliveryAddress['region'])) { + $deliveryObject->getPropByName('region')->setValue($crmDeliveryAddress['region']); + } + + if (!empty($crmDeliveryAddress['city'])) { + $deliveryObject->getPropByName('city')->setValue($crmDeliveryAddress['city']); + } + + if (!empty($crmDeliveryAddress['street'])) { + $deliveryObject->getPropByName('street')->setValue($crmDeliveryAddress['street']); + } + + if (!empty($crmDeliveryAddress['building'])) { + $deliveryObject->getPropByName('house')->setValue($crmDeliveryAddress['building']); + } + + if (!empty($crmDeliveryAddress['flat'])) { + $deliveryObject->getPropByName('flat')->setValue($crmDeliveryAddress['flat']); + } + + $order->getObject()->getPropByName('delivery_address')->setValue($deliveryObject->getId()); + } + + if (!empty($crmOrder['number'])) { + $order->setName($crmOrder['number']); + } else { + $order->generateNumber(); + $order->setName($order->getName() . ' ' . $crmOrder['id'] . "-retailcrm"); + } + + $this->api->ordersFixExternalIds(array( + array( + 'id' => $crmOrder['id'], + 'externalId' => $order->getId() + ) + )); + } else { + if (!$order) { + continue; + } + + if (isset($crmOrder['items']) && count($crmOrder['items']) > 0) { + $orderItems = $order->getItems(); + foreach ($orderItems as $orderItem) { + $order->removeItem($orderItem); + } + + $crmOrderForItems = $this->api->ordersGet($crmOrder['externalId'])->getOrder(); + $crmItems = $crmOrderForItems['items']; + + foreach ($crmItems as $crmItem) { + if (isset($crmItem['deleted']) && $crmItem['deleted'] == true) { + continue; + } + if (!isset($crmItem['offer']['externalId'])) { + continue; + } + + if (mb_strpos($crmItem['offer']['externalId'], '#')) { + $data = explode('#', $crmItem['offer']['externalId']); + $itemId = $data[0]; + + $orderItem = orderItem::create($itemId); + + $itemOptions = $data[1]; + $itemOptions = explode('-', $itemOptions); + foreach ($itemOptions as $itemOption) { + $itemOption = explode('_', $itemOption); + $itemOptionGroupId = $itemOption[0]; + $itemOptionValue = $itemOption[1]; + $itemOptionObject = new umiField($itemOptionGroupId); + + $orderItem->appendOption($itemOptionObject->getName(), $itemOptionValue); + } + } else { + $orderItem = orderItem::create($crmItem['offer']['externalId']); + } + + /** @var optionedOrderItem $orderItem */ + $orderItem->setAmount($crmItem['quantity']); + $order->appendItem($orderItem); + } + } + + $customer = $objects->getObject($order->getCustomerId()); + + if (isset($crmOrder['phone'])) { + $customer->getPropByName('phone')->setValue($crmOrder['phone']); + } + + if (isset($crmOrder['lastName'])) { + $customer->getPropByName('lname')->setValue($crmOrder['lastName']); + } + + if (isset($crmOrder['firstName'])) { + $customer->getPropByName('fname')->setValue($crmOrder['firstName']); + } + + if (isset($crmOrder['patronymic'])) { + $customer->getPropByName('father_name')->setValue($crmOrder['patronymic']); + } + + if (isset($crmOrder['e-mail'])) { + $customer->getPropByName('e-mail')->setValue($crmOrder['e-mail']); + } + + if (isset($crmOrder['paymentType'])) { + $relationOrderPaymentTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentTypeMap')); + $umiOrderPaymentType = $retailcrm->getRelationByMap($relationOrderPaymentTypesMap, $crmOrder['paymentType'], true); + $order->getObject()->getPropByName('payment_id')->setValue($umiOrderPaymentType); + } + + if (isset($crmOrder['paymentStatus'])) { + $relationOrderPaymentStatusesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderPaymentStatusMap')); + $umiOrderPaymentStatus = $retailcrm->getRelationByMap($relationOrderPaymentStatusesMap, $crmOrder['paymentStatus'], true); + $order->getObject()->getPropByName('payment_status_id')->setValue($umiOrderPaymentStatus); + } + + if (isset($crmOrder['delivery']['code'])) { + $relationOrderDeliveryTypesMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderDeliveryTypeMap')); + $umiOrderDeliveryType = $retailcrm->getRelationByMap($relationOrderDeliveryTypesMap, $crmOrder['delivery']['code'], true); + $order->getObject()->getPropByName('delivery_id')->setValue($umiOrderDeliveryType); + } + + if (isset($crmOrder['delivery']['address']) && count($crmOrder['delivery']['address'])) { + $crmDeliveryAddress = $crmOrder['delivery']['address']; + + $deliveryTypeId = $objectTypes->getTypeIdByGUID('emarket-deliveryaddress'); + $deliveryObject = $objects->getObjectByName('Address for order ' . $order->getId()); + if (!$deliveryObject) { + $deliveryObjectId = $objects->addObject('Address for order ' . $order->getId(), $deliveryTypeId); + $deliveryObject = $objects->getObject($deliveryObjectId); + } + + if (!empty($crmDeliveryAddress['countryIso'])) { + $selector = new selector('objects'); + $selector->types('object-type')->id($config->get('retailcrm', 'countryGuideId')); + + $countries = $selector->result(); + + foreach ($countries as $country) { + /** @var umiObject $country */ + + $countryCode = $country->getValue('country_iso_code'); + + if ($crmDeliveryAddress['countryIso'] == $countryCode) { + $deliveryObject->getPropByName('country')->setValue($country->getId()); + break; + } + } + } + + if (!empty($crmDeliveryAddress['index'])) { + $deliveryObject->getPropByName('index')->setValue($crmDeliveryAddress['index']); + } + + if (!empty($crmDeliveryAddress['region'])) { + $deliveryObject->getPropByName('region')->setValue($crmDeliveryAddress['region']); + } + + if (!empty($crmDeliveryAddress['city'])) { + $deliveryObject->getPropByName('city')->setValue($crmDeliveryAddress['city']); + } + + if (!empty($crmDeliveryAddress['street'])) { + $deliveryObject->getPropByName('street')->setValue($crmDeliveryAddress['street']); + } + + if (!empty($crmDeliveryAddress['building'])) { + $deliveryObject->getPropByName('house')->setValue($crmDeliveryAddress['building']); + } + + if (!empty($crmDeliveryAddress['flat'])) { + $deliveryObject->getPropByName('flat')->setValue($crmDeliveryAddress['flat']); + } + + $order->getObject()->getPropByName('delivery_address')->setValue($deliveryObject->getId()); + } + } + + if (!$order) { + continue; + } + + $regedit->setVal('//modules/RetailCRM/IgnoreObjectUpdateEvent/' . $order->getObject()->getId(), time()); + + if (isset($crmOrder['status'])) { + $relationMap = $retailcrm->getRelationMap($config->get('retailcrm', 'orderStatusMap')); + $umiOrderStatusCode = $retailcrm->getRelationByMap($relationMap, $crmOrder['status'], true); + if ($umiOrderStatusCode) { + // меняем дату редактирования заказа, для того, чтобы его не перехватил хенлдер и не выплюнул обратно в црм + $order->getObject()->setUpdateTime(time()); + $order->setOrderStatus($umiOrderStatusCode); + } + } + + $order->refresh(); + $config->set('retailcrm', 'lastHistorySinceId', $lastChange['id']); + } + } + } + + public static function runCustomers() + { + + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmHttpClient.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmHttpClient.php new file mode 100644 index 0000000..d3038fb --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmHttpClient.php @@ -0,0 +1,110 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * @link http://www.retailcrm.ru/docs/Developers/ApiVersion4 + */ +class RCrmHttpClient +{ + const METHOD_GET = 'GET'; + const METHOD_POST = 'POST'; + + protected $url; + protected $defaultParameters; + + /** + * Client constructor. + * + * @param string $url api url + * @param array $defaultParameters array of parameters + * + * @throws InvalidArgumentException + */ + public function __construct($url, array $defaultParameters = array()) + { + if (false === stripos($url, 'https://')) { + throw new InvalidArgumentException( + 'API schema requires HTTPS protocol' + ); + } + + $this->url = $url; + $this->defaultParameters = $defaultParameters; + } + + /** + * Make HTTP request + * + * @param string $path request url + * @param string $method (default: 'GET') + * @param array $parameters (default: array()) + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * + * @throws InvalidArgumentException + * @throws RCrmCurlException + * @throws RCrmJsonException + * + * @return RCrmApiResponse + */ + public function makeRequest( + $path, + $method, + array $parameters = array() + ) { + $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); + + if (!in_array($method, $allowedMethods, false)) { + throw new InvalidArgumentException( + sprintf( + 'Method "%s" is not valid. Allowed methods are %s', + $method, + implode(', ', $allowedMethods) + ) + ); + } + + $parameters = array_merge($this->defaultParameters, $parameters); + + $url = $this->url . $path; + + if (self::METHOD_GET === $method && count($parameters)) { + $url .= '?' . http_build_query($parameters, '', '&'); + } + + $curlHandler = curl_init(); + curl_setopt($curlHandler, CURLOPT_URL, $url); + curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandler, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curlHandler, CURLOPT_FAILONERROR, false); + curl_setopt($curlHandler, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curlHandler, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($curlHandler, CURLOPT_TIMEOUT, 30); + curl_setopt($curlHandler, CURLOPT_CONNECTTIMEOUT, 30); + + if (self::METHOD_POST === $method) { + curl_setopt($curlHandler, CURLOPT_POST, true); + curl_setopt($curlHandler, CURLOPT_POSTFIELDS, $parameters); + } + + $responseBody = curl_exec($curlHandler); + $statusCode = curl_getinfo($curlHandler, CURLINFO_HTTP_CODE); + $errno = curl_errno($curlHandler); + $error = curl_error($curlHandler); + + curl_close($curlHandler); + + if ($errno) { + throw new RCrmCurlException($error, $errno); + } + + return new RCrmApiResponse($statusCode, $responseBody); + } +} diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmIcml.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmIcml.php new file mode 100644 index 0000000..a784184 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmIcml.php @@ -0,0 +1,309 @@ +getList(); + $domainCollection = $domainsCollectionList[1]; + $serverProtocol = mainConfiguration::getInstance()->get('system', 'server-protocol') . '://'; + $this->shopUrl = $serverProtocol . $domainCollection->getHost(); + } + + public function generateICML() + { + $string = ' + + + ' . $this->shopName . ' + + + + + '; + + $xml = new SimpleXMLElement( + $string, + LIBXML_NOENT | LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE + ); + + $this->dd = new DOMDocument(); + $this->dd->preserveWhiteSpace = false; + $this->dd->formatOutput = true; + $this->dd->loadXML($xml->asXML()); + + $this->eCategories = $this->dd->getElementsByTagName('categories')->item(0); + $this->eOffers = $this->dd->getElementsByTagName('offers')->item(0); + + $this->addCategories(); + $this->addOffers(); + + $downloadPath = __DIR__ . '/../../../../../'; + + if (!file_exists($downloadPath)) { + mkdir($downloadPath, 0755); + } + + $this->dd->saveXML(); + $this->dd->save($downloadPath . 'retailcrm.xml'); + } + + /** + * + */ + private function addCategories() + { + $categories = new selector('pages'); + $categories->types('hierarchy-type')->name('catalog', 'category'); + + $result = $categories->result(); + + foreach ($result as $category) { + /** @var umiHierarchyElement $category */ + + /** @var DOMElement $e */ + $e = $this->eCategories->appendChild( + $this->dd->createElement( + 'category', $category->getName() + ) + ); + + $e->setAttribute('id', $category->getId()); + + if ($category->getRel() > 0) { + $e->setAttribute('parentId', $category->getRel()); + } + } + } + + private function getObjectUrl(umiHierarchyElement $obj) + { + $url = '/' . $obj->getAltName(); + + $parent = new umiHierarchyElement($obj->getRel()); + while (true) { + $url = '/' . $parent->getAltName() . $url; + + if ($parent->getRel() != 0) { + $parent = new umiHierarchyElement($parent->getRel()); + } else { + break; + } + } + + $url = $this->shopUrl . $url; + + return $url; + } + + private function getCombinationsFromMultyArray($sourceData) + { + $sourceDataKeys = array(); + foreach ($sourceData as $key => $value) { + $sourceDataKeys[] = $key; + } + + $data = array(); + $data[] = ''; + for ($i = 0; $i < count($sourceData); $i++) { + $oldData = $data; + $data = array(); + + foreach ($oldData as $value) { + foreach ($sourceData[$sourceDataKeys[$i]] as $value2) { + $data[] = (!empty($value) ? $value . ',' : '') . $sourceDataKeys[$i] . '-' . $value2; + } + } + } + + $resultData = array(); + foreach ($data as $value) { + $items = explode(',', $value); + $columns = array(); + + foreach ($items as $item) { + $item = explode('-', $item); + $columns[$item[0]] = $item[1]; + } + + $resultData[] = $columns; + } + + return $resultData; + } + + private function addOffers() + { + $offers = new selector('pages'); + $offers->types('hierarchy-type')->name('catalog', 'object'); + + $result = $offers->result(); + + foreach ($result as $offer) { + /** @var umiHierarchyElement $offer */ + + $objects = umiObjectsCollection::getInstance(); + + $offerObject = new umiObject($offer->getObjectId()); + + /** @var umiFieldsGroup $optionsObject */ + $optionsObject = $offerObject->getType()->getFieldsGroupByName('catalog_option_props'); + + $options = array(); + $optionValues = array(); + $optionGroups = array(); + $optionPrices = array(); + foreach ($optionsObject->getFields() as $optionField) { + /** @var umiField $optionField */ + + $optionGroups[$optionField->getId()] = $optionField; + + $values = $offerObject->getValue($optionField->getName()); + + foreach ($values as $value) { + $valueObject = $objects->getObject($value['rel']); + $options[$optionField->getId()][] = $valueObject->getId(); + + $optionPrices[$valueObject->getId()] = $value['float']; + $optionValues[$valueObject->getId()] = $valueObject; + } + } + + if (count($options)) { + $offerOptions = $this->getCombinationsFromMultyArray($options); + } else { + // Если нет опционных товаров(товарных предложений) передаём массив с 1 пустым элементом - базовый товар + $offerOptions = array(''); + } + + foreach ($offerOptions as $offerOption) { + if (!empty($offerOption)) { + $options = array(); + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $options[] = $offerOptionId . '_' . $offerOptionValue; + } + + $offerId = $offer->getId() . '#' . implode('-', $options); + } else { + $offerId = $offer->getId(); + } + + /** @var DOMElement $e */ + $e = $this->eOffers->appendChild($this->dd->createElement('offer')); + $e->setAttribute('id', $offerId); + $e->setAttribute('productId', $offer->getId()); + $quantity = $offerObject->getPropByName('common_quantity')->getValue(); + $e->setAttribute('quantity', !empty($quantity) ? $quantity : 0); + + /** + * Offer activity + */ + $activity = $offer->getIsActive() == 1 ? 'Y' : 'N'; + $e->appendChild( + $this->dd->createElement('productActivity') + )->appendChild( + $this->dd->createTextNode($activity) + ); + + /** + * Offer category + */ + $e->appendChild($this->dd->createElement('categoryId')) + ->appendChild( + $this->dd->createTextNode($offer->getRel()) + ); + + /** + * Name & price + */ + if (!empty($offerOption)) { + $options = array(); + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $options[] = $optionGroups[$offerOptionId]->getTitle() . ': ' . $optionValues[$offerOptionValue]->getName(); + } + $offerName = $offer->getName() . ' (' . implode(', ', $options) . ')'; + } else { + $offerName = $offer->getName(); + } + + $e->appendChild($this->dd->createElement('name')) + ->appendChild($this->dd->createTextNode($offerName)); + + $e->appendChild($this->dd->createElement('productName')) + ->appendChild($this->dd->createTextNode($offer->getName())); + + $price = $offerObject->getPropByName('price')->getValue(); + + if (!empty($offerOption)) { + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $price += $optionPrices[$offerOptionValue]; + } + } + + $e->appendChild($this->dd->createElement('price')) + ->appendChild($this->dd->createTextNode($price)); + + /** + * Options + */ + if (!empty($offerOption)) { + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $option = $this->dd->createElement('param'); + $option->setAttribute('code', $optionGroups[$offerOptionId]->getName()); + $option->setAttribute('name', $optionGroups[$offerOptionId]->getTitle()); + $option->appendChild($this->dd->createTextNode($optionValues[$offerOptionValue]->getName())); + $e->appendChild($option); + } + } + + /** + * Image + */ + /** @var umiImageFile $photo */ + if ($offerObject->getPropByName('images') !== null) { + $photos = $offerObject->getPropByName('images')->getValue(); + + if (is_array($photos) && count($photos)) { + $photo = reset($photos); + $photoPath = $this->shopUrl . $photo->getFilePath(true); + + $e->appendChild($this->dd->createElement('picture')) + ->appendChild($this->dd->createTextNode($photoPath)); + } + } + + /** + * Url + */ + $url = $this->getObjectUrl($offer); + $e->appendChild($this->dd->createElement('url')) + ->appendChild( + $this->dd->createTextNode($url) + ); + + /** + * Additional characteristics + */ + if ($offerObject->getPropByName('weight')) { + $weight = $this->dd->createElement('param'); + $weight->setAttribute('code', 'weight'); + $weight->setAttribute('name', 'Вес'); + $weight->appendChild($this->dd->createTextNode($offerObject->getPropByName('weight')->getValue() * 1000)); + $e->appendChild($weight); + } + } + } + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RCrmJsonException.php b/classes/modules/RetailCRM/classes/retailcrm/RCrmJsonException.php new file mode 100644 index 0000000..7a34a34 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RCrmJsonException.php @@ -0,0 +1,5 @@ +api = new RCrmApiClient($url, $key); + $this->log = $log; + } + + public function __call($method, $arguments) + { + $accessLog = date('H:m:i') . ' [' . $method . '] -> ' . json_encode($arguments) . "\n"; + error_log($accessLog, 3, $this->log); + + try { + $response = call_user_func_array(array($this->api, $method), $arguments); + if (!$response->isSuccessful()) { + error_log("[$method] " . $response->getErrorMsg() . "\n", 3, $this->log); + if (isset($response['errors'])) { + $error = implode("\n", $response['errors']); + error_log($error . "\n", 3, $this->log); + } + $response = false; + } + return $response; + } catch (RCrmCurlException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } catch (RCrmJsonException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiClient.php b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiClient.php new file mode 100644 index 0000000..a479845 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiClient.php @@ -0,0 +1,811 @@ +client = new RetailcrmHttpClient($url, array('apiKey' => $apiKey)); + $this->siteCode = $site; + } + + /** + * Create a order + * + * @param array $order + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersCreate(array $order, $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + return $this->client->makeRequest("/orders/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'order' => json_encode($order) + ))); + } + + /** + * Edit a order + * + * @param array $order + * @param string $by + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersEdit(array $order, $by = 'externalId', $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($order[$by])) { + throw new InvalidArgumentException(sprintf('Order array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/orders/" . $order[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array( + 'order' => json_encode($order), + 'by' => $by, + )) + ); + } + + /** + * Upload array of the orders + * + * @param array $orders + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersUpload(array $orders, $site = null) + { + if (!sizeof($orders)) { + throw new InvalidArgumentException('Parameter `orders` must contains array of the orders'); + } + + return $this->client->makeRequest("/orders/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'orders' => json_encode($orders), + ))); + } + + /** + * Get order by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/orders/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns a orders history + * + * @param DateTime $startDate (default: null) + * @param DateTime $endDate (default: null) + * @param int $limit (default: 100) + * @param int $offset (default: 0) + * @param bool $skipMyChanges (default: true) + * @return RetailcrmApiResponse + */ + public function ordersHistory( + DateTime $startDate = null, + DateTime $endDate = null, + $limit = 100, + $offset = 0, + $skipMyChanges = true + ) { + $parameters = array(); + + if ($startDate) { + $parameters['startDate'] = $startDate->format('Y-m-d H:i:s'); + } + if ($endDate) { + $parameters['endDate'] = $endDate->format('Y-m-d H:i:s'); + } + if ($limit) { + $parameters['limit'] = (int) $limit; + } + if ($offset) { + $parameters['offset'] = (int) $offset; + } + if ($skipMyChanges) { + $parameters['skipMyChanges'] = (bool) $skipMyChanges; + } + + return $this->client->makeRequest('/orders/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns filtered orders list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns statuses of the orders + * + * @param array $ids (default: array()) + * @param array $externalIds (default: array()) + * @return RetailcrmApiResponse + */ + public function ordersStatuses(array $ids = array(), array $externalIds = array()) + { + $parameters = array(); + + if (sizeof($ids)) { + $parameters['ids'] = $ids; + } + if (sizeof($externalIds)) { + $parameters['externalIds'] = $externalIds; + } + + return $this->client->makeRequest('/orders/statuses', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save order IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function ordersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/orders/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'orders' => json_encode($ids), + )); + } + + /** + * Get orders assembly history + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders/packs/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Create a customer + * + * @param array $customer + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersCreate(array $customer, $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + return $this->client->makeRequest("/customers/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customer' => json_encode($customer) + ))); + } + + /** + * Edit a customer + * + * @param array $customer + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersEdit(array $customer, $by = 'externalId', $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($customer[$by])) { + throw new InvalidArgumentException(sprintf('Customer array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/customers/" . $customer[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite( + $site, + array( + 'customer' => json_encode($customer), + 'by' => $by + ) + ) + ); + } + + /** + * Upload array of the customers + * + * @param array $customers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersUpload(array $customers, $site = null) + { + if (!sizeof($customers)) { + throw new InvalidArgumentException('Parameter `customers` must contains array of the customers'); + } + + return $this->client->makeRequest("/customers/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customers' => json_encode($customers), + ))); + } + + /** + * Get customer by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/customers/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns filtered customers list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function customersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/customers', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save customer IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function customersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/customers/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'customers' => json_encode($ids), + )); + } + + /** + * Get purchace prices & stock balance + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventories(array $filter = array(), $page = null, $limit = null, $site = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/store/inventories', RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, $parameters)); + } + + /** + * Upload store inventories + * + * @param array $offers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventoriesUpload(array $offers, $site = null) + { + if (!sizeof($offers)) { + throw new InvalidArgumentException('Parameter `offers` must contains array of the customers'); + } + + return $this->client->makeRequest( + "/store/inventories/upload", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array('offers' => json_encode($offers))) + ); + } + + /** + * Returns deliveryServices list + * + * @return RetailcrmApiResponse + */ + public function deliveryServicesList() + { + return $this->client->makeRequest('/reference/delivery-services', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns deliveryTypes list + * + * @return RetailcrmApiResponse + */ + public function deliveryTypesList() + { + return $this->client->makeRequest('/reference/delivery-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderMethods list + * + * @return RetailcrmApiResponse + */ + public function orderMethodsList() + { + return $this->client->makeRequest('/reference/order-methods', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderTypes list + * + * @return RetailcrmApiResponse + */ + public function orderTypesList() + { + return $this->client->makeRequest('/reference/order-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentStatuses list + * + * @return RetailcrmApiResponse + */ + public function paymentStatusesList() + { + return $this->client->makeRequest('/reference/payment-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentTypes list + * + * @return RetailcrmApiResponse + */ + public function paymentTypesList() + { + return $this->client->makeRequest('/reference/payment-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns productStatuses list + * + * @return RetailcrmApiResponse + */ + public function productStatusesList() + { + return $this->client->makeRequest('/reference/product-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statusGroups list + * + * @return RetailcrmApiResponse + */ + public function statusGroupsList() + { + return $this->client->makeRequest('/reference/status-groups', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statuses list + * + * @return RetailcrmApiResponse + */ + public function statusesList() + { + return $this->client->makeRequest('/reference/statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns sites list + * + * @return RetailcrmApiResponse + */ + public function sitesList() + { + return $this->client->makeRequest('/reference/sites', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns stores list + * + * @return RetailcrmApiResponse + */ + public function storesList() + { + return $this->client->makeRequest('/reference/stores', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Edit deliveryService + * + * @param array $data delivery service data + * @return RetailcrmApiResponse + */ + public function deliveryServicesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-services/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryService' => json_encode($data) + ) + ); + } + + /** + * Edit deliveryType + * + * @param array $data delivery type data + * @return RetailcrmApiResponse + */ + public function deliveryTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryType' => json_encode($data) + ) + ); + } + + /** + * Edit orderMethod + * + * @param array $data order method data + * @return RetailcrmApiResponse + */ + public function orderMethodsEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-methods/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderMethod' => json_encode($data) + ) + ); + } + + /** + * Edit orderType + * + * @param array $data order type data + * @return RetailcrmApiResponse + */ + public function orderTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderType' => json_encode($data) + ) + ); + } + + /** + * Edit paymentStatus + * + * @param array $data payment status data + * @return RetailcrmApiResponse + */ + public function paymentStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentStatus' => json_encode($data) + ) + ); + } + + /** + * Edit paymentType + * + * @param array $data payment type data + * @return RetailcrmApiResponse + */ + public function paymentTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentType' => json_encode($data) + ) + ); + } + + /** + * Edit productStatus + * + * @param array $data product status data + * @return RetailcrmApiResponse + */ + public function productStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/product-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'productStatus' => json_encode($data) + ) + ); + } + + /** + * Edit order status + * + * @param array $data status data + * @return RetailcrmApiResponse + */ + public function statusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'status' => json_encode($data) + ) + ); + } + + /** + * Edit site + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function sitesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/sites/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'site' => json_encode($data) + ) + ); + } + + /** + * Edit store + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function storesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + if (!isset($data['name'])) { + throw new InvalidArgumentException('Data must contain "name" parameter.'); + } + + return $this->client->makeRequest( + '/reference/stores/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'store' => json_encode($data) + ) + ); + } + + /** + * Update CRM basic statistic + * + * @return RetailcrmApiResponse + */ + public function statisticUpdate() + { + return $this->client->makeRequest('/statistic/update', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Return current site + * + * @return string + */ + public function getSite() + { + return $this->siteCode; + } + + /** + * Set site + * + * @param string $site + * @return void + */ + public function setSite($site) + { + $this->siteCode = $site; + } + + /** + * Check ID parameter + * + * @param string $by + * @return bool + */ + protected function checkIdParameter($by) + { + $allowedForBy = array('externalId', 'id'); + if (!in_array($by, $allowedForBy)) { + throw new InvalidArgumentException(sprintf( + 'Value "%s" for parameter "by" is not valid. Allowed values are %s.', + $by, + implode(', ', $allowedForBy) + )); + } + + return true; + } + + /** + * Fill params by site value + * + * @param string $site + * @param array $params + * @return array + */ + protected function fillSite($site, array $params) + { + if ($site) { + $params['site'] = $site; + } elseif ($this->siteCode) { + $params['site'] = $this->siteCode; + } + + return $params; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiResponse.php b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiResponse.php new file mode 100644 index 0000000..6d169d2 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmApiResponse.php @@ -0,0 +1,122 @@ +statusCode = (int) $statusCode; + + if (!empty($responseBody)) { + $response = json_decode($responseBody, true); + + if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { + throw new InvalidJsonException( + "Invalid JSON in the API response body. Error code #$error", + $error + ); + } + + $this->response = $response; + } + } + + /** + * Return HTTP response status code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * HTTP request was successful + * + * @return bool + */ + public function isSuccessful() + { + return $this->statusCode < 400; + } + + /** + * Allow to access for the property throw class method + * + * @param string $name + * @return mixed + */ + public function __call($name, $arguments) + { + // convert getSomeProperty to someProperty + $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); + + if (!isset($this->response[$propertyName])) { + throw new InvalidArgumentException("Method \"$name\" not found"); + } + + return $this->response[$propertyName]; + } + + /** + * Allow to access for the property throw object property + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!isset($this->response[$name])) { + throw new InvalidArgumentException("Property \"$name\" not found"); + } + + return $this->response[$name]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('This activity not allowed'); + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + throw new BadMethodCallException('This call not allowed'); + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->response[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + if (!isset($this->response[$offset])) { + throw new InvalidArgumentException("Property \"$offset\" not found"); + } + + return $this->response[$offset]; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHelpers.php b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHelpers.php new file mode 100644 index 0000000..aad9620 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHelpers.php @@ -0,0 +1,41 @@ + ', $mapItem); + $map[$mapItem[0]] = $mapItem[1]; + } + + return $map; + } + + /** + * @param $map array + * @param $item string + * @param $reversed bool + * @return string|null + */ + public function getRelationByMap($map, $item, $reversed = false) { + if(!$reversed) { + if(isset($map[$item]) && !empty($map[$item])) + return $map[$item]; + else + return null; + } else { + foreach ($map as $umiStatusOrder => $crmStatusOrder) { + if($crmStatusOrder == $item) + return $umiStatusOrder; + } + return null; + } + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHttpClient.php b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHttpClient.php new file mode 100644 index 0000000..820d518 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmHttpClient.php @@ -0,0 +1,113 @@ +url = $url; + $this->defaultParameters = $defaultParameters; + $this->retry = 0; + } + + /** + * Make HTTP request + * + * @param string $path + * @param string $method (default: 'GET') + * @param array $parameters (default: array()) + * @param int $timeout + * @param bool $verify + * @param bool $debug + * @return RetailcrmApiResponse + */ + public function makeRequest( + $path, + $method, + array $parameters = array(), + $timeout = 30, + $verify = false, + $debug = false + ) { + $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); + if (!in_array($method, $allowedMethods)) { + throw new InvalidArgumentException(sprintf( + 'Method "%s" is not valid. Allowed methods are %s', + $method, + implode(', ', $allowedMethods) + )); + } + + $parameters = array_merge($this->defaultParameters, $parameters); + + $url = $this->url . $path; + + if (self::METHOD_GET === $method && sizeof($parameters)) { + $url .= '?' . http_build_query($parameters, '', '&'); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify); + + if (!$debug) { + curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); + } else { + curl_setopt($ch, CURLOPT_TIMEOUT_MS, (int) $timeout + ($this->retry * 2000)); + } + + if (self::METHOD_POST === $method) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); + } + + $responseBody = curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($errno && in_array($errno, array(6, 7, 28, 34, 35)) && $this->retry < 3) { + $errno = null; + $error = null; + $this->retry += 1; + $this->makeRequest( + $path, + $method, + $parameters, + $timeout, + $verify, + $debug + ); + } + + if ($errno) { + throw new CurlException($error, $errno); + } + + return new RetailcrmApiResponse($statusCode, $responseBody); + } + + public function getRetry() + { + return $this->retry; + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/RetailcrmProxy.php b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmProxy.php new file mode 100644 index 0000000..e866baf --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/RetailcrmProxy.php @@ -0,0 +1,39 @@ +api = new RetailcrmApiClient($url, $key); + $this->log = $log; + } + public function __call($method, $arguments) + { + $accessLog = date('H:m:i') . ' [' . $method . '] -> ' . json_encode($arguments) . "\n"; + error_log($accessLog, 3, $this->log); + + try { + $response = call_user_func_array(array($this->api, $method), $arguments); + if (!$response->isSuccessful()) { + error_log("[$method] " . $response->getErrorMsg() . "\n", 3, $this->log); + if (isset($response['errors'])) { + $error = implode("\n", $response['errors']); + error_log($error . "\n", 3, $this->log); + } + $response = false; + } + return $response; + } catch (CurlException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } catch (InvalidJsonException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } + } +} \ No newline at end of file diff --git a/classes/modules/RetailCRM/classes/retailcrm/icml.php b/classes/modules/RetailCRM/classes/retailcrm/icml.php new file mode 100644 index 0000000..cc20895 --- /dev/null +++ b/classes/modules/RetailCRM/classes/retailcrm/icml.php @@ -0,0 +1,294 @@ +getList(); + $domainCollection = $domainsCollectionList[1]; + $serverProtocol = mainConfiguration::getInstance()->get('system', 'server-protocol') . '://'; + $this->shopUrl = $serverProtocol . $domainCollection->getHost(); + } + + public function generateICML() + { + $string = ' + + + '.$this->shopName.' + + + + + '; + $xml = new SimpleXMLElement( + $string, + LIBXML_NOENT |LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE + ); + $this->dd = new DOMDocument(); + $this->dd->preserveWhiteSpace = false; + $this->dd->formatOutput = true; + $this->dd->loadXML($xml->asXML()); + $this->eCategories = $this->dd + ->getElementsByTagName('categories')->item(0); + $this->eOffers = $this->dd + ->getElementsByTagName('offers')->item(0); + + $this->addCategories(); + $this->addOffers(); + $this->dd->saveXML(); + $downloadPath = __DIR__ . '/../../../../../'; + if (!file_exists($downloadPath)) { + mkdir($downloadPath, 0755); + } + $this->dd->save($downloadPath . 'retailcrm.xml'); + } + + /** + * + */ + private function addCategories() + { + $categories = new selector('pages'); + $categories->types('hierarchy-type')->name('catalog', 'category'); + + $result = $categories->result(); + + foreach($result as $category) { + /** @var umiHierarchyElement $category */ + + /** @var DOMElement $e */ + $e = $this->eCategories->appendChild( + $this->dd->createElement( + 'category', $category->getName() + ) + ); + $e->setAttribute('id', $category->getId()); + if ($category->getRel() > 0) { + $e->setAttribute('parentId', $category->getRel()); + } + } + } + private function getObjectUrl(umiHierarchyElement $obj) { + $url = ''; + $url = '/' . $obj->getAltName() . $url; + + $parent = new umiHierarchyElement($obj->getRel()); + while(true) { + $url = '/' . $parent->getAltName() . $url; + + if($parent->getRel() != 0) { + $parent = new umiHierarchyElement($parent->getRel()); + } else { + break; + } + } + + $url = $this->shopUrl . $url; + + return $url; + } + + private function getCombinationsFromMultyArray($sourceData) { + $sourceDataKeys = array(); + foreach ($sourceData as $key=>$value) { + $sourceDataKeys[] = $key; + } + + $data = array(); + $data[] = ''; + for($i = 0; $i < count($sourceData); $i++) { + $oldData = $data; + $data = array(); + + foreach($oldData as $value) { + foreach ($sourceData[$sourceDataKeys[$i]] as $value2) { + $data[] = (!empty($value) ? $value.',' : '') . $sourceDataKeys[$i] . '-' . $value2; + } + } + } + + $resultData = array(); + foreach ($data as $value) { + $items = explode(',', $value); + $columns = array(); + foreach ($items as $item) { + $item = explode('-', $item); + $columns[$item[0]] = $item[1]; + } + $resultData[] = $columns; + } + + return $resultData; + } + + private function addOffers() + { + $offers = new selector('pages'); + $offers->types('hierarchy-type')->name('catalog', 'object'); + + $result = $offers->result(); + + foreach ($result as $offer) { + /** @var umiHierarchyElement $offer */ + + $objects = umiObjectsCollection::getInstance(); + + $offerObject = new umiObject($offer->getObjectId()); + + /** @var umiFieldsGroup $optionsObject */ + $optionsObject = $offerObject->getType()->getFieldsGroupByName('catalog_option_props'); + + $options = array(); + $optionValues = array(); + $optionGroups = array(); + $optionPrices = array(); + foreach ($optionsObject->getFields() as $optionField) { + /** @var umiField $optionField */ + + $optionGroups[$optionField->getId()] = $optionField; + + $values = $offerObject->getValue($optionField->getName()); + + foreach ($values as $value) { + $valueObject = $objects->getObject($value['rel']); + $options[$optionField->getId()][] = $valueObject->getId(); + + $optionPrices[$valueObject->getId()] = $value['float']; + + $optionValues[$valueObject->getId()] = $valueObject; + } + } + + if(count($options)) + $offerOptions = $this->getCombinationsFromMultyArray($options); + else { + // Если нет опционных товаров(товарных предложений) передаём массив с 1 пустым элементом - базовый товар + $offerOptions = array(); + $offerOptions[] = ''; + } + + foreach ($offerOptions as $offerOption) { + if(!empty($offerOption)) { + $options = array(); + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $options[] = $offerOptionId . '_' . $offerOptionValue; + } + + $offerId = $offer->getId() . '#' . implode('-', $options); + } else + $offerId = $offer->getId(); + + + /** @var DOMElement $e */ + $e = $this->eOffers->appendChild($this->dd->createElement('offer')); + $e->setAttribute('id', $offerId); + $e->setAttribute('productId', $offer->getId()); + $quantity = $offerObject->getPropByName('common_quantity')->getValue(); + $e->setAttribute('quantity', !empty($quantity) ? $quantity : 0); + + /** + * Offer activity + */ + $activity = $offer->getIsActive() == 1 ? 'Y' : 'N'; + $e->appendChild( + $this->dd->createElement('productActivity') + )->appendChild( + $this->dd->createTextNode($activity) + ); + + /** + * Offer category + */ + $e->appendChild($this->dd->createElement('categoryId')) + ->appendChild( + $this->dd->createTextNode($offer->getRel()) + ); + + /** + * Name & price + */ + if(!empty($offerOption)) { + $options = array(); + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $options[] = $optionGroups[$offerOptionId]->getTitle() . ': ' . $optionValues[$offerOptionValue]->getName(); + } + $offerName = $offer->getName() . ' (' . implode(', ', $options) . ')'; + } else + $offerName = $offer->getName(); + $e->appendChild($this->dd->createElement('name')) + ->appendChild($this->dd->createTextNode($offerName)); + + $e->appendChild($this->dd->createElement('productName')) + ->appendChild($this->dd->createTextNode($offer->getName())); + + $price = $offerObject->getPropByName('price')->getValue(); + if(!empty($offerOption)) { + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $price += $optionPrices[$offerOptionValue]; + } + } + $e->appendChild($this->dd->createElement('price')) + ->appendChild($this->dd->createTextNode($price)); + + /** + * Options + */ + if(!empty($offerOption)) { + $options = array(); + foreach ($offerOption as $offerOptionId => $offerOptionValue) { + $option = $this->dd->createElement('param'); + $option->setAttribute('code', $optionGroups[$offerOptionId]->getName()); + $option->setAttribute('name', $optionGroups[$offerOptionId]->getTitle()); + $option->appendChild($this->dd->createTextNode($optionValues[$offerOptionValue]->getName())); + $e->appendChild($option); + } + } + + /** + * Image + */ + /** @var umiImageFile $photo */ + $photo = $offerObject->getPropByName('photo')->getValue(); + $photoPath = $photo->getFilePath(true); + $photoFullPath = $this->shopUrl . $photoPath; + $e->appendChild($this->dd->createElement('picture')) + ->appendChild($this->dd->createTextNode($photoFullPath)); + + /** + * Url + */ + $url = $this->getObjectUrl($offer); + $e->appendChild($this->dd->createElement('url')) + ->appendChild( + $this->dd->createTextNode($url) + ); + + /** + * Additional characteristics + */ + if ($offerObject->getPropByName('weight')) { + $weight = $this->dd->createElement('param'); + $weight->setAttribute('code', 'weight'); + $weight->setAttribute('name', 'Вес'); + $weight->appendChild($this->dd->createTextNode($offerObject->getPropByName('weight')->getValue() * 1000)); + $e->appendChild($weight); + } + } + } + } +} + +?> \ No newline at end of file diff --git a/classes/modules/RetailCRM/data/objects.xml b/classes/modules/RetailCRM/data/objects.xml new file mode 100644 index 0000000..3226d51 --- /dev/null +++ b/classes/modules/RetailCRM/data/objects.xml @@ -0,0 +1,100 @@ + + + + id + firstName + lastName + patronymic + email + birthday + phones + manager + commentary + externalId + cumulativeDiscount + personalDiscount + discountCardNumber + + index + country + region + city + street + building + house + block + flat + floor + intercomCode + metro + notes + + contragentType + legalName + legalAddress + certificateNumber + certificateDate + bank + bankAddress + corrAccount + bankAccount + + id + createdAt + orderType + orderMethod + site + status + manager + firstName + lastName + patronymic + phone + additionalPhone + email + paymentType + paymentStatus + paymentDetail + discount + discountPercent + prepaySum + customerComment + managerComment + shipmentStore + shipmentDate + shipped + + + id + initialPrice + discount + discountPercent + quantity + status + + code + service + date + time + cost + netCost + + country + index + region + city + street + building + house + block + flat + floor + intercomCode + metro + notes + + status + trackNumber + courier + + \ No newline at end of file diff --git a/classes/modules/RetailCRM/events.php b/classes/modules/RetailCRM/events.php new file mode 100644 index 0000000..43a9abd --- /dev/null +++ b/classes/modules/RetailCRM/events.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/classes/modules/RetailCRM/i18n.php b/classes/modules/RetailCRM/i18n.php new file mode 100644 index 0000000..c37bc46 --- /dev/null +++ b/classes/modules/RetailCRM/i18n.php @@ -0,0 +1,28 @@ + 'Ключ API', + 'option-crmUrl' => 'Url RetailCRM API', + + 'module-RetailCRM' => 'RetailCRM', + 'header-RetailCRM-manage' => 'Управление', + + 'group-access' => 'Настройка доступа', + 'group-guidesMapping' => 'Справочники', + 'group-orderStatusesMapping' => 'Статусы', + 'group-orderPaymentsMapping' => 'Способы оплаты', + 'group-orderPaymentStatusesMapping' => 'Статусы оплаты', + 'group-orderDeliveryTypesMapping' => 'Способы доставки', + 'group-incorrect-data' => 'Проверьте корректность заполненных полей', + + 'option-country' => 'Страны', + + 'notification-status-payment-initialized' => 'ожидает оплаты', + 'notification-status-payment-accepted' => 'оплачен', + 'notification-status-payment-validated' => 'обновлен: оплата подтверждена', + 'notification-status-payment-declined' => 'обновлен: оплата отклонена', + + 'notification-status-delivery-waiting_shipping' => 'ожидает отгрузки', + 'notification-status-delivery-shipping' => 'доставляется', + 'notification-status-delivery-ready' => 'доставлен', +); \ No newline at end of file diff --git a/classes/modules/RetailCRM/install.php b/classes/modules/RetailCRM/install.php new file mode 100644 index 0000000..fe9ba43 --- /dev/null +++ b/classes/modules/RetailCRM/install.php @@ -0,0 +1,26 @@ +5;1{*V-ElT4Npl#K~#9!?Ob_qQ&kr~_q~_30cP$-Llf&*>YO2Gn^0#;g}Ey>IF-n;jH zeNrFi?hC{ z6M^#^xBuD6gG_u}9O!hG1q(d<9Ho>`N|OHkvpOf_hLd5~Fj`&?lBBtE#WN>LDHQ}t zDXFON#i{OKqGzbPS}l?e9N@KDUaRF35O zc6*|t=+&P1#6%m*n3!j%yMSpW0E~=7hl)S^KuSt*UY;Q-iCQdPmwHr!vvP1jf$5`< zEEyTpCwX8b9zSkpr;m*#Ja2t#oHa|#&aN#Z@z^mt7hSh**1yT)$Myxf?%h2ys-`uJ zfn@=J*J|BkfxqhK6B0BRF4!UzLO=*=-I|n?Ahj9*H2L}d*%7^W4|V7e0QEa_p;cA3 z@b}6qjmNSZov$@jM)=}I~)1*nhIC=ACVcRz94|!3)Bmu`+lZx>D z_oTR3{^5sA=g$7wk#zJZH)Mz{0JxGSHy0SDR+omaU9-ibUcK~japvXAZ6WE&6Mbx~ z9}$utJlG!CjN``D9wT=~scF_M=dewTMw`5)rPk7>=86loYX<D}8UFC0GXza0UHJ9k>X{1N~t z&odo6(npT~AvPgFwP{mr?Z`NGta{HL0H6kgAvxJ@I9zhF^6Rhta}oZfm#ClQ)z?UI zvAA!a^CyPUqs{Bq`OV0MsZK{J75D6^3)rDh8~D7|;I1qrP<;{i-&xRy56M6OfKCSt zgCZhO$Brm0>_4#cpKwu;K0aPNbjbHI@zkk0F!#^P_};z4&Yrc_>_MX13S=BLO0{DL z(rBEwE5=Ea$mPo*lL11G&j|rSWNB%Ng$sivkfDG7u(M|!g;RF6dH#Is<_Q2m5Ky~z zsC8=uKuSw#c{#{rR%?*YoGJI#D5|qj^8*KJoTM=f8bo>CDvfg4GH%2O&tI#kfQX2| zdl@yE6jP@<>WR!u%JWuzX)a&(_02j-Q?o!~W~O}dWQVe{va0U|0f5}RX&gNo5K^vP z3m7|D1QPe`!Iv(DWoM(-tpgws8y!vV-eo#=wDEuLIU90CMK0mBYgdnZa8go&FJIPN zyckHMHl(D4ojT<(si!*tHZf84?YGas?aAMNlk3;H)Kt4w84?q1n;b&8Aw!g_SG&iE zapTDK>rA(9>defbxINCzCD*RWoQz4_zTL8Rt8k ztGN03(uotKqy$mQbneWirm{VH)L$+$PMvCAv7&}7cJicY;X+y`s(Zyq?%Wa5)A6NC z?29k5Nl8pp6qO`=>lXgw4_aI-n=wOJw@#Cv?+ECj5;i2z|EWL?%cz-~?qzOsW zSUL)Xkm6$c@F55SsMV-#Tieawdd9}Gc(EZq-aLOkDJ?~9+MrIIP(%dI%QKB1Z%9rS z_wKEi%z&|Z)+}09rrNj97i1+R_PtTC%2O#Xk55V>RaG=N-S-$xO zMA5x67eyvITD5g6YSk(z(%5fjJ3dKDav9z5MDj6X)vLBQX0!Uh0j66w z7Yn;|$uwaCAf&M`3z!uXA{#TtIUe9ES8SIa78d4daO)ot6_n}pAxE>_H)Yx&{}`&-rK%}>9_ z7zYpbrQ*`kpj4vb{{0TUwra&Z^(InLVMtCUB_-~8?dVZHA;FNES_gq@ku}eqLjY{Q ze)e3!Yqc&DErI|_CDX1QDJcO$935Rj2>b0f6d4)dio(#ZpG(On2!@mtUvr26Kq=F+ zr~1bqgHoj%UVSwzC&vY|Lx)Ur=Xx#}EEaWE7SpLyu=Wh3pg`EMqk4-UC>1j^@x_Z& zuVsr=Y5oH}KkGKB_{{A{or40^4W8VtTy@dF0n+qZ)+4W*P)(}D%Y zv^0G4rq^k=-MjU1ak?&D>Z{i;ic(IFW#vlLvL&04z{bQNjfRw$;WCj&kDMpB^@)kp|1D`SJG7MgTOS6qf8&3t8tQF@qzOsWl%;8KH(f)z z3ZWB1CxlK2oe+i438520ryhxWRzFO69_Qxb%a>_!F)1sfm6c#J0YbKkRg3{+;5gKx z1&WMhqN13dJ=s2ed>_$k+_f@3e_mL(4&S_qTC|Xj7$F-u5=BJ;!vI2FH}aI)UPdGS z{dZyaZhY+;6BVWY_1C6=4t?}UI(X2Mk->EAD4#Had;RrBj2vkeB)vn)%N zNI5y`0|(shUc10|?V5SnGIIAWn9cr)?#f%2d0Q-svhm{;bLN0t-muXzn@uxkqVRC#^5x*^W*@nE z(==@wEi0=I95ou5mMyuI6ec#7iH=5*kw~KfjsuLTUQf%)NO3XF&&OxZP`%#9oKoO8 z#V4Q0-hR6g*p6lSa$H!b+Pc-_F;HpWKJ&6=?i!{PsZ@%E3%Q{~YpZ-w6f!b|ty@*= z*RyUD9Ss&8eEz&?+B8*0hVw9*F{Ab zCdRyI5qk-R>$0O+mgMF0HjtjI&#FaYL)eTviS4Q;{N@}?J#-u z3IHLH%RwfS4jq!RvXm=Ucs*cU|L8Eyn^Q>wo`-6OK|!qsmC7v`(|`Y^7<;>1Zs<^? zR$JDtr8*rqc(CH*kD;dNVIe(T*tH8-RyKUN;a?gd~;V3E! z^75R~Ar%$)*fH+SH@z`r2M$yZ99Vk+*ujJ4&6~@ePRBQ_jBXq?3bkpY+PKkU$TZKL zYky?gQ~#0F2M;pQ(M>;R0f1uZQheb8_Hn|xMN6gBi4^Ns){Dpa`ND<`(wQ?zqw)ICK3_Rp008;>@5{!GtL5`f8VWi9 zKr1VaBStV?yQ;qX&UbGR2*GF0i2L?Qxw+P7200AJ?sV1{!yt`@8!|*Tc5GcvCN>;& zY*v5zw0Y@LHYrK5V1fPNGmVjgcu#^97MkYF0gM$37I5C54_jNSG@B8})wYM<@X>K> z)_3j{Hf}@_5%Sry*}i@2VojnbY~Czv*#a0-LEyAnZo~*~$Po1W^Nk)IhsWjRNk@-L z$B!dQnfQ2Ct7UriV!L*A-;q@}ocs52Q4zU*T{?Emy7fol;mVaOgWV*2MpKoPf+&I@ z;Hy`0ZZ0k;Ab0N2%1SVs?WZS!6D}2#U%LPR36n`gK~!ko?OJ8|7e7Q$U(j8z{!KX#-I!9tkR*d(S=hJbvH) zI2Z1N3-@xlAUVI|kHh)>e)sqLK0e>q`4L2f;fa9}$vdo_ITK_Y2g&79>+8{c*h)z7XyHE*|H#GrlKNe zRu)K-a`b4Rn1u9nM0DijgvACEA%EfO)R*L@TCDNa1|CJqEcHcq^EoS zQd2P)`K=V5cmffHv@}OX28JU4(-NW|0}O_cd~&}3*cuyMyLQ=HTh*ExU3mZinr8d` z_t1QLtgyUX`Sn+%6gADYXAkY~AGJ|NU!P1gZ6LyBVs0)_3IL!e;>L}kM{t z5=345!h!{a=RGQZ<{8tzeIxDWK5|62H6bm{<7*^|-nbFMM19ffN0bJ<-~x9S1h1D! zPbVf50H7q(|xW*+UCX(sX^=H;@ zHy=3?LT~En)8XhXIDDKrIY=o|O1rv*MT??_9cDuD7BMj}vBZsb0ljujYih#l*Fn=r zTpY7x37eKSiuK`GhT761?$|LDL=jL3{n7L1ky3Wh(*r2qd?Oz{3ZjUV0?+fymmBx) z1*0(v_TtLRGaoyqKdUECN`-~MFaQ9u%p@fdtCe28ig)e+!vLcB_;GgLyvXIL)YqdV z>CfV;uj+AEIdy7`@*H+QU#trA=L_lS?qkQi{;;PfAaipcJ}mCqHOja?vgjF~$vm&S zR4=*R#B3geJb(EGolcLB+OkDRPZt&~QZHT{NBb1>^DP6pp;BJ%E-v;As4DP0Nl2hq zt`Mu$lTIEuz^zy@R)2<_o%H&3_VLHPcql8AtE%AkZPYYku`mxkWd8EYDC3=Wbvd7V z4rMvGWhEu4b#>9fdoWbGw3Zg^>Oxs22?H5KV8Qw;2SxU9GN0Me(K)s){#m2tJ*K2t%DOiCecS zKmR=TJSFsIg58c$q7XWBu3BYlZuZLjufL@I`?c0qP*p+*#KoER>|wLBqmdEi=bHz+ z9{|o3E3mg0ST=B;s#@#n28)nLOdp+2e#;hb$+d7H-ns=W3zU+W7-s%_X3iXNy8!?= z&b58}n5#0-Y;;ys{7r51{J?j7QM*u*#o5aO=J;{j^peTNYYDx;;z8wi? z*w+Vx*<*h{0QhE)^uh)Ad+)j5f1miYlbD1AM`mWs<;$bqjERp2RRxao*7K6&9ZgL& zeeeOdYL%yn1ptUbO3Db;{zzhE9(zo!uJ#n~I%W+9?Cu1`Oj|zI?pm8JnFAIx2XcdE^mYR<7rt2Y;w2m6Z;=eXQ;ZT`sM@p3BYk zc#z~|7?2VQ0yA$Oc6I`Q?!5@lTYvk_Pq3rnjF`=;53p<(FES54=oyNM!JZyqS$!w$ z?KOS!Nywc-r*h7vORWtJK7K_MUw#??{TD@%uq;fSYS_A!-?S-ek=pU(lhKN2;YI50 z+tr4KphNDmGS|EB(%#;vZKmfSjx}qf;$n0Xl!0iUcKfq85h@P7T3#pS`($1ZHR#pIG*xfC? z{q~?&K!?MyX_MurpTc1b0LZjyQDWdLHc$z^4(4IAj+e}~n`x^zivYJzBYHzJ8|w57%U@yDj(ViFtc_x{{Dar0)d z*?e;mVzn~K$=u47+`4td=e8dH^N;fV_v)E5w7uQ)-NI-z78LL=yl|IuF15B+E-5j8 z^_B0}6`kZyKb1fK93%;N-cJ>T0HvU5001nj-&pd`))W#KXDBG(H*5$@$0ojhbFN%z z*u0s~&kqvD`umk{zjYrxNB~0I7M3LPzyrp8`?#zu2{6ZLL&XY<%mjkuLyY zPY>e-oA}}eE>kLRyI4EwzjJE^~T-1`4?ZDlzNMpWLd4M sq8%NE-Mc5NO-?ep-!s3@+hn8v0MVmt6ePL_AYuRj1TRTMK~zY`y;e(T6I~QO_s%3uZGREP2Nhq7f>xVkViqmBD+s01 zVnvYRbD>bE3tdDAg@TKsQc%%CT_{2YA)v0h62&6;C{jrJpkz@&8mgdGawnPRz2{%d z)HY4hB$)sI2;|H;bIv{QI|LCS7u5M<2M(yAkjq0vMAYxz#buCu#kDNgkQxl;SV2U6 zXsCdR0wxQ)<{;bE}Eut0z{ zHT?wQDN?Bt>9NPh5m7yQlm!Ag0?R6WJ|dzJ z#_ihwF?)_;bE|CAOs}I{Sg8HA|BVH(Mn?|4Ux^Va9I5Mh1Evs+7q1@L>5tQ(Ia? z-=E&U-;Bj_W_n^mq|-T#^7Cgp7!=>Wm2-UY&bKVG`ld^#0e}!fcI;STW-`XWfR($@ z$mY$SP{_D*=Wmmr?dcKUzq?T;671kXrYNknwThj;-rUzdeQG{@=+IJgv*-MIFip^P zwYk|C8UjsQwGg=Y@+EWpxOVAMRkxmLYFDn*E;|8+p+}=uEJn6%14JMMY#S&AO5H2o zvTAPJ@N{=qbLra8o?%^`r?Yd-=Lw&i!=)ucDQ##Vd-wjgJD-30WW{3gkt1@!;DpGY zI6*dTl24vo>uB|_9LwUfvzSWZ@887hg{@m@Ve zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00m)5L_t(o!|j;OYgAVl#((cQ z_f95BO~!mEN&Lu{B}P!AZj|amS}OQ~t|BhAe?b3Li(a?|Hj8&Wu*UW^`Hw4-EG%E_0sqp67Yr=Y2(0hk;H$?v)K*g8bNl zV)#VXXP}>fe(Wpw5Pzbo{v}BuioiJ#0c#-)K?D)`e}QVDfSa7eWwYq~JS;DJz#<-G zT^**a4YO$zlF3vIxMH9h$GCg z4_UpsqWwfwCsvy9-P|1U$Oz=~n4TW&)~%3EBj6)h=7fJZhrW1$tXfrRM67zEI#k|V6$Q`FB4g34$tdbZH4UHiq549Z9D>4IVu3AV)_0H#Iew{(ekfAF^f*Bndh-MRNN# zIzA3@{F=b3a`kR%3RT5)cY_F8C=mVe2g#*N{)5(5!sEvY4;(;RT6{zS3I*K61o8Rv zn2rvDg9j_V`ga0V=Wu`iiD_?#RLTRNm>?b=_6o15Asieeb>c)R+ND)ni`~50A219{ z;;T-rk0M+yhuOFh5kZ|Jxp@=KwdLjjgO;`ANxfA%Fj(rQ8;~?(>+P-d&1pg&UlHux>&5vYfl7Tnq*CbI9Aq*O1emTa54ce9!g~4?X>Kk@v-8dR zA%N-VAT>CM)YVl%^+AAXYr{QwfT|+KV76>QT3dap8XYA*dloHn`5#m$DqZt=M5H2+ z{|cxWgW0?potY^K%l7tS_wDnbmX?T4pQiBp@3>qJEmj~v7Z*ux-D3HtpD6tLD{g+i zx~9Z*bdVTBa{oSd%NC#Hj~zqD#z?MSMRPf#pMNH~aG_j6qX;)KfxCShot`eWB&m}p zOJ3o_yQg#-^W~Sg8#l0>oyZqoV0(KBzxxitkod|KSX{(izfLkb>J_buI#EGay2E_;A_gO->S@J4bx+B04$g^K7wavKH#?Y>>EsR8HEd>);h1y$mq zA*8(>yJJVm8~i7Iy;zIw>%;W)piiFQvRPkF@7^sdJC#COTCjWepmTHBfq{xW>3dH~ zBn*+YYmv2Uu|<6+a8WVsvNB>B=0vQ`#c~04D?f& fIA=croY}tti@JGHROO&A00000NkvXXu0mjf6L`{u literal 0 HcmV?d00001