* @license MIT * @link http://retailcrm.ru * @see http://retailcrm.ru/docs */ use Bitrix\Sale\PersonType; use Intaro\RetailCrm\Component\ServiceLocator; use Bitrix\Sale\Delivery\Services\EmptyDeliveryService; use Bitrix\Sale\Internals\OrderPropsTable; use Bitrix\Sale\Internals\StatusTable; use Bitrix\Sale\PaySystem\Manager; use Intaro\RetailCrm\Service\Utils; use RetailCrm\Exception\CurlException; use RetailCrm\Exception\InvalidJsonException; use Intaro\RetailCrm\Service\ManagerService; use Bitrix\Main\UserFieldTable; use Bitrix\Main\UserFieldLangTable; use Bitrix\Sale\Internals\SiteCurrencyTable; IncludeModuleLangFile(__FILE__); require_once __DIR__ . '/../../lib/component/servicelocator.php'; require_once __DIR__ . '/../../lib/service/utils.php'; /** * class RCrmActions * * @category RetailCRM * @package RetailCRM */ class RCrmActions { public static $MODULE_ID = 'intaro.retailcrm'; public static $CRM_ORDER_FAILED_IDS = 'order_failed_ids'; public static $CRM_API_VERSION = 'api_version'; public static function getCurrencySites(): array { $sites = self::getSitesList(); $baseCurrency = CCurrency::GetBaseCurrency(); $sitesCurrency = []; foreach ($sites as $site) { $siteCurrency = SiteCurrencyTable::getCurrency($site['LID']); $sitesCurrency[$site['LID']] = !empty($siteCurrency['CURRENCY']) ? $siteCurrency['CURRENCY'] : $baseCurrency; } return $sitesCurrency; } /** * @return array */ public static function getSitesList(): array { $arSites = []; $rsSites = CSite::GetList($by, $sort, ['ACTIVE' => 'Y']); while ($ar = $rsSites->Fetch()) { $arSites[] = $ar; } return $arSites; } public static function OrderTypesList($arSites) { $orderTypesList = []; foreach ($arSites as $site) { $personTypes = PersonType::load($site['LID']); $bitrixOrderTypesList = []; foreach ($personTypes as $personType) { if (!array_key_exists($personType['ID'], $orderTypesList)) { $bitrixOrderTypesList[$personType['ID']] = $personType; } asort($bitrixOrderTypesList); } $orderTypesList += $bitrixOrderTypesList; } return $orderTypesList; } public static function DeliveryList() { $bitrixDeliveryTypesList = []; $arDeliveryServiceAll = \Bitrix\Sale\Delivery\Services\Manager::getActiveList(); $noOrderId = EmptyDeliveryService::getEmptyDeliveryServiceId(); $groups = []; foreach ($arDeliveryServiceAll as $arDeliveryService) { if ($arDeliveryService['CLASS_NAME'] == '\Bitrix\Sale\Delivery\Services\Group') { $groups[] = $arDeliveryService['ID']; } } foreach ($arDeliveryServiceAll as $arDeliveryService) { if ((($arDeliveryService['PARENT_ID'] == '0' || $arDeliveryService['PARENT_ID'] == null) || in_array($arDeliveryService['PARENT_ID'], $groups)) && $arDeliveryService['ID'] != $noOrderId && $arDeliveryService['CLASS_NAME'] != '\Bitrix\Sale\Delivery\Services\Group') { if (in_array($arDeliveryService['PARENT_ID'], $groups)) { $arDeliveryService['PARENT_ID'] = 0; } $bitrixDeliveryTypesList[] = $arDeliveryService; } } return $bitrixDeliveryTypesList; } public static function PaymentList() { $bitrixPaymentTypesList = []; $dbPaymentAll = Manager::getList(['select' => ['ID', 'NAME'], 'filter' => ['ACTIVE' => 'Y']]); while ($payment = $dbPaymentAll->fetch()) { $bitrixPaymentTypesList[] = $payment; } return $bitrixPaymentTypesList; } public static function StatusesList() { $bitrixPaymentStatusesList = []; $obStatuses = StatusTable::getList([ 'filter' => ['TYPE' => 'O', '=Bitrix\Sale\Internals\StatusLangTable:STATUS.LID' => LANGUAGE_ID], 'select' => ['ID', 'NAME' => 'Bitrix\Sale\Internals\StatusLangTable:STATUS.NAME'], ]); while ($arStatus = $obStatuses->fetch()) { $bitrixPaymentStatusesList[$arStatus['ID']] = ['ID' => $arStatus['ID'], 'NAME' => $arStatus['NAME']]; } return $bitrixPaymentStatusesList; } public static function OrderPropsList() { $bitrixPropsList = []; $arPropsAll = OrderPropsTable::getList([ 'select' => ['*'], 'filter' => [ ['CODE' => '_%'], ['!=TYPE' => 'LOCATION'] ] ]); while ($prop = $arPropsAll->Fetch()) { $bitrixPropsList[$prop['PERSON_TYPE_ID']][] = $prop; } return $bitrixPropsList; } public static function getLocationProps() { $bitrixPropsList = []; $arPropsAll = OrderPropsTable::getList([ 'select' => ['*'], 'filter' => [ ['CODE' => '_%'], ['TYPE' => 'LOCATION'] ] ]); while ($prop = $arPropsAll->Fetch()) { $bitrixPropsList[$prop['PERSON_TYPE_ID']][] = $prop; } return $bitrixPropsList; } public static function PricesExportList() { $catalogExportPrices = []; $dbPriceType = CCatalogGroup::GetList([], [], false, false, ['ID', 'NAME', 'NAME_LANG']); while ($arPriceType = $dbPriceType->Fetch()) { $catalogExportPrices[$arPriceType['ID']] = $arPriceType; } return $catalogExportPrices; } public static function StoresExportList() { $catalogExportStores = []; $dbStores = CCatalogStore::GetList([], ['ACTIVE' => 'Y'], false, false, ['ID', 'TITLE']); while ($stores = $dbStores->Fetch()) { $catalogExportStores[] = $stores; } return $catalogExportStores; } public static function IblocksExportList() { $catalogExportIblocks = []; $dbIblocks = CIBlock::GetList(['IBLOCK_TYPE' => 'ASC', 'NAME' => 'ASC'], ['CHECK_PERMISSIONS' => 'Y', 'MIN_PERMISSION' => 'W']); while ($iblock = $dbIblocks->Fetch()) { if ($arCatalog = CCatalog::GetByIDExt($iblock['ID'])) { if($arCatalog['CATALOG_TYPE'] == 'D' || $arCatalog['CATALOG_TYPE'] == 'X' || $arCatalog['CATALOG_TYPE'] == 'P') { $catalogExportIblocks[$iblock['ID']] = [ 'ID' => $iblock['ID'], 'IBLOCK_TYPE_ID' => $iblock['IBLOCK_TYPE_ID'], 'LID' => $iblock['LID'], 'CODE' => $iblock['CODE'], 'NAME' => $iblock['NAME'], ]; if ($arCatalog['CATALOG_TYPE'] == 'X' || $arCatalog['CATALOG_TYPE'] == 'P') { $iblockOffer = CCatalogSKU::GetInfoByProductIBlock($iblock['ID']); $catalogExportIblocks[$iblock['ID']]['SKU'] = $iblockOffer; } } } } return $catalogExportIblocks; } /** * * w+ event in bitrix log */ public static function eventLog($auditType, $itemId, $description) { CEventLog::Add([ 'SEVERITY' => 'SECURITY', 'AUDIT_TYPE_ID' => $auditType, 'MODULE_ID' => self::$MODULE_ID, 'ITEM_ID' => $itemId, 'DESCRIPTION' => $description, ]); } /** * * Agent function * * @return self name */ public static function uploadOrdersAgent() { RetailCrmOrder::uploadOrders(); $failedIds = unserialize(COption::GetOptionString(self::$MODULE_ID, self::$CRM_ORDER_FAILED_IDS, 0)); if (is_array($failedIds) && !empty($failedIds)) { RetailCrmOrder::uploadOrders(50, true); } return 'RCrmActions::uploadOrdersAgent();'; } /** * * Agent function * * @return self name * * @throws \Throwable */ public static function orderAgent() { if (COption::GetOptionString('main', 'agents_use_crontab', 'N') !== 'N') { define('NO_AGENT_CHECK', true); } try { $service = ManagerService::getInstance(); $service->synchronizeManagers(); RetailCrmHistory::customerHistory(); RetailCrmHistory::orderHistory(); } catch (\Throwable $exception) { Logger::getInstance()->write( 'Fail orderAgent:' . PHP_EOL . $exception->getMessage() . PHP_EOL . 'File: ' . $exception->getFile() . PHP_EOL . 'Line: ' . $exception->getLine() . PHP_EOL, 'orderAgent' ); } return 'RCrmActions::orderAgent();'; } /** * removes all empty fields from arrays * working with nested arrs * * @param array $arr * * @return array */ public static function clearArr(array $arr): array { /** @var Utils $utils */ $utils = ServiceLocator::getOrCreate(Utils::class); return $utils->clearArray($arr); } /** * * @param array|bool|\SplFixedArray|string $str in SITE_CHARSET * * @return array|bool|\SplFixedArray|string $str in utf-8 */ public static function toJSON($str) { /** @var Utils $utils */ $utils = ServiceLocator::getOrCreate(Utils::class); return $utils->toUTF8($str); } /** * * @param string|array|\SplFixedArray $str in utf-8 * * @return array|bool|\SplFixedArray|string $str in SITE_CHARSET */ public static function fromJSON($str) { if ($str === null) { return ''; } /** @var Utils $utils */ $utils = ServiceLocator::getOrCreate(Utils::class); return $utils->fromUTF8($str); } /** * Extracts payment ID or client ID from payment externalId * Payment ID - pass nothing or 'id' as second argument * Client ID - pass 'client_id' as second argument * * @param $externalId * @param string $data * @return bool|string */ public static function getFromPaymentExternalId($externalId, $data = 'id') { switch ($data) { case 'id': if (false === strpos($externalId, '_')) { return $externalId; } else { return substr($externalId, 0, strpos($externalId, '_')); } break; case 'client_id': if (false === strpos($externalId, '_')) { return ''; } else { return substr($externalId, strpos($externalId, '_'), count($externalId)); } break; } return ''; } /** * Returns true if provided externalId in new format (id_clientId) * * @param $externalId * @return bool */ public static function isNewExternalId($externalId) { return !(false === strpos($externalId, '_')); } /** * Generates payment external ID * * @param $id * * @return string */ public static function generatePaymentExternalId($id) { return sprintf( '%s_%s', $id, COption::GetOptionString(self::$MODULE_ID, 'client_id', 0) ); } /** * Unserialize array * * @param string $string * * @return mixed */ public static function unserializeArrayRecursive($string) { if ($string === false || empty($string)) { return false; } if (is_string($string)) { $string = unserialize($string); } if (!is_array($string)) { $string = self::unserializeArrayRecursive($string); } return $string; } /** * @param string|null $fio * * @return array */ public static function explodeFio(?string $fio): array { $result = []; $fio = preg_replace('|[\s]+|s', ' ', trim($fio)); if (empty($fio)) { return $result; } else { $newFio = explode(' ', $fio, 3); } switch (count($newFio)) { default: case 0: $result['firstName'] = $fio; break; case 1: $result['firstName'] = $newFio[0]; break; case 2: $result = [ 'lastName' => $newFio[0], 'firstName' => $newFio[1], ]; break; case 3: $result = [ 'lastName' => $newFio[0], 'firstName' => $newFio[1], 'patronymic' => $newFio[2], ]; break; } return $result; } public static function customOrderPropList() { $typeMatched = [ 'STRING' => 'STRING', 'NUMBER' => 'NUMERIC', 'Y/N' => 'BOOLEAN', 'DATE' => 'DATE' ]; //Базовые свойства заказа и используемые свойства в функционале модуля $bannedCodeList = [ 'FIO', 'EMAIL', 'PHONE', 'ZIP', 'CITY', 'LOCATION', 'ADDRESS', 'COMPANY', 'COMPANY_ADR', 'INN', 'KPP', 'CONTACT_PERSON', 'FAX', 'LP_BONUS_INFO', 'LP_DISCOUNT_INFO', '' ]; $listPersons = PersonType::getList([ 'select' => ['ID', 'NAME'], 'filter' => ['ENTITY_REGISTRY_TYPE' => 'ORDER'] ])->fetchAll(); $persons = []; foreach ($listPersons as $person) { $persons[$person['ID']] = $person['NAME']; } $propsList = OrderPropsTable::getList([ 'select' => ['ID', 'CODE', 'NAME', 'PERSON_TYPE_ID', 'TYPE'], 'filter' => [ ['!=CODE' => $bannedCodeList], ['?TYPE' => 'STRING | NUMBER | Y/N | DATE'], ['MULTIPLE' => 'N'], ['ACTIVE' => 'Y'] ] ])->fetchAll(); $resultList = []; foreach ($propsList as $prop) { $type = $typeMatched[$prop['TYPE']] ?? $prop['TYPE']; $key = $prop['ID'] . '#' . $prop['CODE']; $resultList[$type . '_TYPE'][$key] = $prop['NAME'] . ' (' . $persons[$prop['PERSON_TYPE_ID']] . ')'; } ksort($resultList); return $resultList; } public static function customUserFieldList() { $typeMatched = [ 'string' => 'STRING', 'double' => 'NUMERIC', 'boolean' => 'BOOLEAN', 'date' => 'DATE', 'integer' => 'INTEGER' ]; $userFields = UserFieldTable::getList([ 'select' => ['ID', 'FIELD_NAME', 'USER_TYPE_ID'], 'filter' => [ ['ENTITY_ID' => 'USER'], ['?FIELD_NAME' => '~%INTARO%'], ['!=FIELD_NAME' => 'UF_SUBSCRIBE_USER_EMAIL'], ['!=USER_TYPE_ID' => 'datetime'], ['?USER_TYPE_ID' => 'string | date | integer | double | boolean'], ['MULTIPLE' => 'N'], ] ])->fetchAll(); $resultList = []; foreach ($userFields as $userField) { $label = UserFieldLangTable::getList([ 'select' => ['EDIT_FORM_LABEL'], 'filter' => [ ["USER_FIELD_ID" => $userField['ID']], ['LANGUAGE_ID' => LANGUAGE_ID] ] ])->fetch(); $type = $typeMatched[$userField['USER_TYPE_ID']] ?? $userField['USER_TYPE_ID']; $resultList[$type . '_TYPE'][$userField['FIELD_NAME']] = $label['EDIT_FORM_LABEL']; } ksort($resultList); return $resultList; } public static function getTypeUserField() { $userFields = UserFieldTable::getList([ 'select' => ['FIELD_NAME', 'USER_TYPE_ID'], 'filter' => [ ['ENTITY_ID' => 'USER'], ['?FIELD_NAME' => '~%INTARO%'], ['!=FIELD_NAME' => 'UF_SUBSCRIBE_USER_EMAIL'], ['?USER_TYPE_ID' => 'string | date | datetime | integer | double | boolean'], ['MULTIPLE' => 'N'], ] ])->fetchAll(); $result = []; foreach ($userFields as $userField) { $result[$userField['FIELD_NAME']] = $userField['USER_TYPE_ID']; } return $result; } public static function convertCmsFieldToCrmValue($value, $type) { $result = $value; switch ($type) { case 'boolean': $result = $value === '1' ? 1 : 0; break; case 'Y/N': $result = $result === 'Y' ? 1 : 0; break; case 'STRING': case 'string': $result = strlen($value) <= 500 ? $value : ''; break; case 'datetime': $result = date('Y-m-d', strtotime($value)); break; } return $result; } public static function convertCrmValueToCmsField($crmValue, $type) { $result = $crmValue; switch ($type) { case 'Y/N': case 'boolean': $result = $crmValue == 1 ? 'Y' : 'N'; break; case 'DATE': case 'date': if (empty($crmValue)) { return ''; } try { $result = date('d.m.Y', strtotime($crmValue)); } catch (\Exception $exception) { $result = ''; } break; case 'STRING': case 'string': case 'text': $result = strlen($crmValue) <= 500 ? $crmValue : ''; break; } return $result; } public static function sendConfiguration($api, $active = true) { $scheme = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; $baseUrl = $scheme . $_SERVER['HTTP_HOST']; $integrationCode = 'bitrix'; $logo = 'https://s3.eu-central-1.amazonaws.com/retailcrm-billing/images/5af47fe682bf2-1c-bitrix-logo.svg'; $accountUrl = $baseUrl . '/bitrix/admin'; $clientId = COption::GetOptionString(self::$MODULE_ID, 'client_id', 0); if (!$clientId) { $clientId = uniqid(); COption::SetOptionString(self::$MODULE_ID, 'client_id', $clientId); } $code = $integrationCode . '-' . $clientId; $configuration = [ 'clientId' => $clientId, 'code' => $code, 'integrationCode' => $integrationCode, 'active' => $active, 'name' => GetMessage('API_MODULE_NAME'), 'logo' => $logo, 'baseUrl' => $baseUrl, 'accountUrl' => $accountUrl ]; self::apiMethod($api, 'integrationModulesEdit', __METHOD__, $configuration); } public static function apiMethod($api, $methodApi, $method, $params, $site = null) { switch ($methodApi) { case 'ordersPaymentDelete': case 'ordersHistory': case 'customerHistory': case 'ordersFixExternalIds': case 'customersFixExternalIds': case 'customersCorporateContacts': case 'customersList': case 'customersCorporateList': return self::proxy($api, $methodApi, $method, [$params]); case 'orderGet': return self::proxy($api, 'ordersGet', $method, [$params, 'id', $site]); case 'ordersGet': case 'ordersEdit': case 'customersGet': case 'customersEdit': case 'customersCorporateGet': return self::proxy($api, $methodApi, $method, [$params, 'externalId', $site]); case 'customersCorporateGetById': return self::proxy($api, 'customersCorporateGet', $method, [$params, 'id', $site]); case 'customersGetById': return self::proxy($api, 'customersGet', $method, [$params, 'id', $site]); case 'paymentEditById': return self::proxy($api, 'ordersPaymentEdit', $method, [$params, 'id', $site]); case 'paymentEditByExternalId': return self::proxy($api, 'ordersPaymentEdit', $method, [$params, 'externalId', $site]); case 'customersCorporateEdit': return self::proxy($api, 'customersCorporateEdit', $method, [$params, 'externalId', $site]); case 'cartGet': return self::proxy($api, $methodApi, $method, [$params, $site, 'externalId']); case 'cartSet': case 'cartClear': return self::proxy($api, $methodApi, $method, [$params, $site]); default: return self::proxy($api, $methodApi, $method, [$params, $site]); } } private static function proxy($api, $methodApi, $method, $params) { $version = COption::GetOptionString(self::$MODULE_ID, self::$CRM_API_VERSION, 0); try { $result = call_user_func_array([$api, $methodApi], $params); if (!$result) { $err = new RuntimeException( $methodApi . ': Got null instead of valid result!' ); Logger::getInstance()->write(sprintf( '%s%s%s', $err->getMessage(), PHP_EOL, $err->getTraceAsString() ), 'apiErrors'); return false; } if ($result->getStatusCode() !== 200 && $result->getStatusCode() !== 201) { if ($methodApi == 'ordersGet' || $methodApi == 'customersGet' || $methodApi == 'customersCorporateGet' ) { Logger::getInstance()->write([ 'api' => $version, 'methodApi' => $methodApi, 'errorMsg' => !empty($result['errorMsg']) ? $result['errorMsg'] : '', 'errors' => !empty($result['errors']) ? $result['errors'] : '', 'params' => $params ], 'apiErrors'); } elseif ($methodApi == 'customersUpload' || $methodApi == 'ordersUpload') { Logger::getInstance()->write([ 'api' => $version, 'methodApi' => $methodApi, 'errorMsg' => !empty($result['errorMsg']) ? $result['errorMsg'] : '', 'errors' => !empty($result['errors']) ? $result['errors'] : '', 'params' => $params ], 'uploadApiErrors'); } elseif ($methodApi == 'cartGet') { Logger::getInstance()->write( [ 'api' => $version, 'methodApi' => $methodApi, 'errorMsg' => !empty($result['errorMsg']) ? $result['errorMsg'] : '', 'errors' => !empty($result['errors']) ? $result['errors'] : '', 'params' => $params, ], 'apiErrors' ); } else { self::eventLog( __CLASS__ . '::' . $method, 'RetailCrm\ApiClient::' . $methodApi, !empty($result['errorMsg']) ? $result['errorMsg'] : '' ); Logger::getInstance()->write([ 'api' => $version, 'methodApi' => $methodApi, 'errorMsg' => !empty($result['errorMsg']) ? $result['errorMsg'] : '', 'errors' => !empty($result['errors']) ? $result['errors'] : '', 'params' => $params, ], 'apiErrors'); } if (function_exists('retailCrmApiResult')) { retailCrmApiResult($methodApi, false, $result->getStatusCode()); } if ($result->getStatusCode() == 460) { return true; } return false; } } catch (CurlException $e) { static::logException( $method, $methodApi, 'CurlException', 'CurlException', $e, $version, $params ); return false; } catch (InvalidArgumentException $e) { static::logException( $method, $methodApi, 'InvalidArgumentException', 'ArgumentException', $e, $version, $params ); return false; } catch (InvalidJsonException $e) { static::logException( $method, $methodApi, 'InvalidJsonException', 'ArgumentException', $e, $version, $params ); } if (function_exists('retailCrmApiResult')) { retailCrmApiResult($methodApi, true, isset($result) ? $result->getStatusCode() : 0); } return isset($result) ? $result : false; } /** * Log exception into log file and event log * * @param string $method * @param string $methodApi * @param string $exceptionName * @param string $apiResultExceptionName * @param \Exception|\Error|\Throwable $exception * @param string $version * @param array $params */ protected static function logException( $method, $methodApi, $exceptionName, $apiResultExceptionName, $exception, $version, $params ) { self::eventLog( __CLASS__ . '::' . $method, 'RetailCrm\ApiClient::' . $methodApi . '::' . $exceptionName, $exception->getCode() . ': ' . $exception->getMessage() ); Logger::getInstance()->write([ 'api' => $version, 'methodApi' => $methodApi, 'errorMsg' => $exception->getMessage(), 'errors' => $exception->getCode(), 'params' => $params ], 'apiErrors'); if (function_exists('retailCrmApiResult')) { retailCrmApiResult($methodApi, false, $apiResultExceptionName); } } }