diff --git a/resources/pot/retailcrm-ru_RU.pot b/resources/pot/retailcrm-ru_RU.pot index 67e62fb..208ff6b 100644 --- a/resources/pot/retailcrm-ru_RU.pot +++ b/resources/pot/retailcrm-ru_RU.pot @@ -483,3 +483,84 @@ msgstr "Вставьте условия обработки персональн msgid "To activate the loyalty program it is necessary to activate the 'enable use of coupons option'" msgstr "Для активации программы лояльности необходимо активировать опцию 'включить использование купонов'" + +msgid "Bonus account" +msgstr "Бонусный счёт" + +msgid "Participation ID: " +msgstr "ID участия: " + +msgid "Current level: " +msgstr "Текущий уровень: " + +msgid "Bonuses on the account: " +msgstr "Бонусов на счёте: " + +msgid "Bonus card number: " +msgstr "Номер бонусной карты: " + +msgid "Date of registration: " +msgstr "Дата регистрации: " + +msgid "Current level rules" +msgstr "Правила текущего уровня" + +msgid "Required amount of purchases to move to the next level: " +msgstr "Необходимая сумма покупок для перехода на следующий уровень: " + +msgid "Activate participation in the loyalty program" +msgstr "Активировать участие в программе лояльности" + +msgid "Send" +msgstr "Отправить" + +msgid "To register in the loyalty program, fill in the form:" +msgstr "Для регистрации в программе лояльности заполните форму:" + +msgid " I agree with " +msgstr " Я согласен с " + +msgid "loyalty program terms" +msgstr "условиями программы лояльности" + +msgid "terms of personal data processing" +msgstr "условиями обработки персональных данных" + +msgid "Phone" +msgstr "Телефон" + +msgid "Error while registering in the loyalty program. Try again later" +msgstr "Ошибка при регистрации в программе лояльности. Попробуйте позже" + +msgid "The card is not linked" +msgstr "Карта не привязана" + +msgid "Error while retrieving data. Try again later" +msgstr "Ошибка при получении данных. Попробуйте позже" + +msgid "Error when activating the loyalty program. Try again later" +msgstr "Ошибка при активации программы лояльности. Попробуйте позже" + +msgid "Enter the correct phone number" +msgstr "Введите корректный номер телефона" + +msgid "Close" +msgstr "Закрыть" + +msgid "Ordinary products: accrual of 1 bonus for each %s %s" +msgstr "Обычные товары: начисление 1 бонуса за каждые %s %s" + +msgid "Promotional products: accrual of 1 bonus for each %s %s" +msgstr "Акционные товары: начисление 1 бонуса за каждые %s %s" + +msgid "Ordinary products: bonus accrual in the amount of %s%% of the purchase amount" +msgstr "Обычные товары: начисление бонусов в размере %s%% от суммы покупки" + +msgid "Promotional products: bonus accrual in the amount of %s%% of the purchase amount" +msgstr "Акционные товары: начисление бонусов в размере %s%% от суммы покупки" + +msgid "Ordinary products: %s%% discount" +msgstr "Обычные товары: %s%% скидка" + +msgid "Promotional products: %s%% discount" +msgstr "Акционные товары: %s%% скидка" diff --git a/src/assets/css/retailcrm-loyalty-style.css b/src/assets/css/retailcrm-loyalty-style.css new file mode 100644 index 0000000..933467b --- /dev/null +++ b/src/assets/css/retailcrm-loyalty-style.css @@ -0,0 +1,34 @@ +.popup-fade-loyalty { + display: none; +} +.popup-fade-loyalty:before { + content: ''; + background: #000; + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 0.7; + z-index: 9999; +} +.popup-loyalty { + position: fixed; + top: 20%; + left: 50%; + padding: 20px; + width: 1000px; + margin-left: -500px; + height: 50%; + background: #fff; + border: 1px solid orange; + border-radius: 4px; + z-index: 99999; + opacity: 1; + overflow: auto; +} +.popup-close-loyalty { + position: absolute; + top: 10px; + right: 10px; +} diff --git a/src/assets/js/retailcrm-loyalty-actions.js b/src/assets/js/retailcrm-loyalty-actions.js new file mode 100644 index 0000000..256fadf --- /dev/null +++ b/src/assets/js/retailcrm-loyalty-actions.js @@ -0,0 +1,121 @@ +jQuery(function() { + jQuery('#loyaltyRegisterForm').on("submit", function (event) { + var termsCheck = jQuery('#termsLoyalty'); + var privacyCheck = jQuery('#privacyLoyalty'); + + if (termsCheck.length) { + if (!termsCheck.is(':checked')) { + event.preventDefault(); + return false; + } + } + + if (privacyCheck.length) { + if (!privacyCheck.is(':checked')) { + event.preventDefault(); + return false; + } + } + + let phone = jQuery('#phoneLoyalty'); + + if (!phone.val().match(/(?:\+|\d)[\d\-\(\) ]{7,}\d/)) { + + if (!jQuery('#warningLoyaltyPhone').length) { + phone.parent().append('' + messagePhone + '') + } + + event.preventDefault(); + return false; + } else { + jQuery('#warningLoyaltyPhone').remove(); + } + + jQuery.ajax({ + url: loyaltyUrl.url + '/admin-ajax.php?action=register_customer_loyalty', + method: 'POST', + timeout: 0, + data: {ajax: 1, phone: phone.val(), userId: customerId}, + dataType: 'json' + }) + .done(function (response) { + if (response.hasOwnProperty('error')) { + jQuery('#loyaltyRegisterForm').append('
'+ response.error + '
') + + event.preventDefault(); + return false; + } else { + location.reload(); + } + }) + + event.preventDefault(); + }); + + jQuery('#loyaltyActivateForm').on("submit", function (event) { + var activateCheckbox = jQuery('#loyaltyActiveCheckbox'); + + if (activateCheckbox.length) { + if (!activateCheckbox.is(':checked')) { + event.preventDefault(); + return false; + } + } + + jQuery.ajax({ + url: loyaltyUrl.url + '/admin-ajax.php?action=activate_customer_loyalty', + method: 'POST', + timeout: 0, + data: {ajax: 1, loyaltyId: loyaltyId}, + dataType: 'json' + }) + .done(function (response) { + if (response.hasOwnProperty('error')) { + jQuery('#loyaltyRegisterForm').append(''+ response.error + '
') + + event.preventDefault(); + return false; + } else { + location.reload(); + } + }) + + event.preventDefault(); + }); + + jQuery('.popup-open-loyalty').click(function() { + if (jQuery(this).attr('id') === 'terms-popup') { + jQuery('#popup-loyalty-text').html(termsLoyalty); + } else { + jQuery('#popup-loyalty-text').html(privacyLoyalty); + } + + jQuery('.popup-fade-loyalty').fadeIn(); + return false; + }); + + jQuery('.popup-close-loyalty').click(function() { + jQuery(this).parents('.popup-fade-loyalty').fadeOut(); + return false; + }); + + jQuery(document).keydown(function(e) { + if (e.keyCode === 27) { // Key Escape + e.stopPropagation(); + jQuery('.popup-fade-loyalty').fadeOut(); + } + }); + + jQuery('.popup-fade-loyalty').click(function(e) { + if (jQuery(e.target).closest('.popup-loyalty').length == 0) { + jQuery(this).fadeOut(); + } + }); + + jQuery('#phoneLoyalty').keydown(function (e) { + let key = e.key; + + return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')'|| key == '-' || + key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backspace'; + }); +}); diff --git a/src/include/api/class-wc-retailcrm-client-v5.php b/src/include/api/class-wc-retailcrm-client-v5.php index 540172a..485db22 100644 --- a/src/include/api/class-wc-retailcrm-client-v5.php +++ b/src/include/api/class-wc-retailcrm-client-v5.php @@ -2972,7 +2972,7 @@ class WC_Retailcrm_Client_V5 $parameters['status'] = $status; return $this->client->makeRequest( - "/api/v5/loyalty/account/$clientIdLoyalty/bonus/$status/details", + "/loyalty/account/$clientIdLoyalty/bonus/$status/details", WC_Retailcrm_Request::METHOD_GET, $parameters ); diff --git a/src/include/class-wc-retailcrm-base.php b/src/include/class-wc-retailcrm-base.php index 6144498..68e0821 100644 --- a/src/include/class-wc-retailcrm-base.php +++ b/src/include/class-wc-retailcrm-base.php @@ -33,6 +33,9 @@ if (!class_exists('WC_Retailcrm_Base')) { /** @var WC_Retailcrm_Cart */ protected $cart; + /** @var WC_Retailcrm_Loyalty */ + protected $loyalty; + /** * Init and hook in the integration. * @@ -100,6 +103,15 @@ if (!class_exists('WC_Retailcrm_Base')) { add_action('admin_enqueue_scripts', [$this, 'include_files_for_admin'], 101); add_action('woocommerce_new_order', [$this, 'create_order'], 11, 1); + + if (isset($this->settings['loyalty']) && $this->settings['loyalty'] === static::YES) { + add_action('wp_ajax_register_customer_loyalty', [$this, 'register_customer_loyalty']); + add_action('wp_ajax_activate_customer_loyalty', [$this, 'activate_customer_loyalty']); + add_action('init', [$this, 'add_loyalty_endpoint'], 11, 1); + add_action('woocommerce_account_menu_items', [$this, 'add_loyalty_item'], 11, 1); + add_action('woocommerce_account_loyalty_endpoint', [$this, 'show_loyalty'], 11, 1); + } + // Subscribed hooks add_action('register_form', [$this, 'subscribe_register_form'], 99); add_action('woocommerce_register_form', [$this, 'subscribe_woocommerce_register_form'], 99); @@ -128,6 +140,8 @@ if (!class_exists('WC_Retailcrm_Base')) { add_action('woocommerce_cart_emptied', [$this, 'clear_cart']); } + $this->loyalty = new WC_Retailcrm_Loyalty($this->apiClient, $this->settings); + // Deactivate hook add_action('retailcrm_deactivate', [$this, 'deactivate']); @@ -624,6 +638,48 @@ if (!class_exists('WC_Retailcrm_Base')) { wp_die(); } + public function register_customer_loyalty() + { + $phone = filter_input(INPUT_POST, 'phone'); + $userId = filter_input(INPUT_POST, 'userId'); + $site = $this->apiClient->getSingleSiteForKey(); + $isSuccessful = false; + + if (!empty($site) && $userId && $phone) { + $isSuccessful = $this->loyalty->registerCustomer($userId, $phone, $site); + } + + if (!$isSuccessful) { + writeBaseLogs('Errors when registering a loyalty program. Passed parameters: ' . + json_encode(['site' => $site, 'userId' => $userId, 'phone' => $phone]) + ); + echo json_encode(['error' => __('Error while registering in the loyalty program. Try again later', 'retailcrm')]); + } else { + echo json_encode(['isSuccessful' => true]); + } + + wp_die(); + } + + public function activate_customer_loyalty() + { + $loyaltyId = filter_input(INPUT_POST, 'loyaltyId'); + $isSuccessful = false; + + if ($loyaltyId) { + $isSuccessful = $this->loyalty->activateLoyaltyCustomer($loyaltyId); + } + + if (!$isSuccessful) { + writeBaseLogs('Errors when activate loyalty program. Passed parameters: ' . json_encode(['loyaltyId' => $loyaltyId])); + echo json_encode(['error' => __('Error when activating the loyalty program. Try again later', 'retailcrm')]); + } else { + echo json_encode(['isSuccessful' => true]); + } + + wp_die(); + } + /** * In this method we include CSS file * @@ -821,6 +877,53 @@ if (!class_exists('WC_Retailcrm_Base')) { wp_die(); } + public function add_loyalty_item($items) + { + $items['loyalty'] = __('Loyalty program', 'retailcrm'); + + return $items; + } + + public function add_loyalty_endpoint() + { + add_rewrite_endpoint('loyalty', EP_PAGES); + } + + public function show_loyalty() + { + $userId = get_current_user_id(); + + if (!isset($userId)) { + return; + } + + $jsScript = 'retailcrm-loyalty-actions'; + $loyaltyUrl = ['url' => get_admin_url()]; + $jsScriptsPath = plugins_url() . '/woo-retailcrm/assets/js/'; + $cssPath = plugins_url() . '/woo-retailcrm/assets/css/'; + $messagePhone = __('Enter the correct phone number', 'retailcrm'); + + wp_register_script($jsScript, $jsScriptsPath . $jsScript . '.js', false, '0.1'); + wp_enqueue_script($jsScript, $jsScriptsPath . $jsScript . '.js', '', '', true); + wp_localize_script($jsScript, 'loyaltyUrl', $loyaltyUrl); + wp_localize_script($jsScript, 'customerId', $userId); + wp_localize_script($jsScript, 'messagePhone', $messagePhone); + wp_localize_script($jsScript, 'termsLoyalty', $this->settings['loyalty_terms']); + wp_localize_script($jsScript, 'privacyLoyalty', $this->settings['loyalty_personal']); + wp_register_style('retailcrm-loyalty-style', $cssPath . 'retailcrm-loyalty-style.css', false, '0.1'); + wp_enqueue_style('retailcrm-loyalty-style'); + + $result = $this->loyalty->getForm($userId); + + if ([] === $result) { + echo ''. __('Error while retrieving data. Try again later', 'retailcrm') . '
'; + } else { + wp_localize_script($jsScript, 'loyaltyId', $result['loyaltyId'] ?? null); + echo $result['form']; + } + } + + /** * Get custom fields with CRM * diff --git a/src/include/class-wc-retailcrm-loyalty.php b/src/include/class-wc-retailcrm-loyalty.php new file mode 100644 index 0000000..936ec3d --- /dev/null +++ b/src/include/class-wc-retailcrm-loyalty.php @@ -0,0 +1,121 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + class WC_Retailcrm_Loyalty + { + /** @var WC_Retailcrm_Client_V5 */ + protected $apiClient; + + protected $dateFormat; + + protected $settings; + + /** @var WC_Retailcrm_Loyalty_Form */ + protected $loyaltyForm; + + public function __construct($apiClient, $settings) + { + $this->apiClient = $apiClient; + $this->settings = $settings; + $this->dateFormat = 'Y-m-d H:i:sP'; + $this->loyaltyForm = new WC_Retailcrm_Loyalty_Form(); + } + + public function getForm(int $userId) + { + $result = []; + + try { + $response = $this->apiClient->customersGet($userId); + + if (!isset($response['customer']['id'])) { + return $result; + } + + $filter['customerId'] = $response['customer']['id']; + + $response = $this->apiClient->getLoyaltyAccountList($filter); + } catch (Throwable $exception) { + writeBaseLogs('Exception get loyalty accounts: ' . $exception->getMessage()); + + return $result; + } + + if (!$response->isSuccessful() || !$response->offsetExists('loyaltyAccounts')) { + return $result; + } + + $loyaltyAccount = $response['loyaltyAccounts'][0] ?? null; + + if ($loyaltyAccount && (int) $loyaltyAccount['customer']['externalId'] === $userId) { + if ($loyaltyAccount['active'] === true) { + $result['form'] = $this->loyaltyForm->getInfoLoyalty($loyaltyAccount); + } else { + $result['form'] = $this->loyaltyForm->getActivationForm(); + + $result['loyaltyId'] = $loyaltyAccount['id']; + } + } else { + $result['form'] = $this->loyaltyForm->getRegistrationForm(); + } + + return $result; + } + + public function registerCustomer(int $userId, string $phone, string $site): bool + { + $parameters = [ + 'phoneNumber' => $phone, + 'customer' => [ + 'externalId' => $userId + ] + ]; + + try { + $response = $this->apiClient->createLoyaltyAccount($parameters, $site); + + if (!$response->isSuccessful()) { + writeBaseLogs('Error while registering in the loyalty program: ' . $response->getRawResponse()); + } + + return $response->isSuccessful(); + } catch (Throwable $exception) { + writeBaseLogs('Exception while registering in the loyalty program: ' . $exception->getMessage()); + + return false; + } + } + + public function activateLoyaltyCustomer(int $loyaltyId) + { + try { + $response = $this->apiClient->activateLoyaltyAccount($loyaltyId); + + if (!$response->isSuccessful()) { + writeBaseLogs('Error while registering in the loyalty program: ' . $response->getRawResponse()); + } + + return $response->isSuccessful(); + } catch (Throwable $exception) { + writeBaseLogs('Exception while activate loyalty account: ' . $exception->getMessage()); + + return false; + } + } + } + +endif; diff --git a/src/include/components/class-wc-retailcrm-loyalty-form.php b/src/include/components/class-wc-retailcrm-loyalty-form.php new file mode 100644 index 0000000..0e600db --- /dev/null +++ b/src/include/components/class-wc-retailcrm-loyalty-form.php @@ -0,0 +1,90 @@ + +%s
+%s%s.
+%s%s.
+ + + +$line
"; + } + + return $result; + } + } + + +endif; diff --git a/src/languages/retailcrm-ru_RU.mo b/src/languages/retailcrm-ru_RU.mo index 6aaec63..ff0da70 100644 Binary files a/src/languages/retailcrm-ru_RU.mo and b/src/languages/retailcrm-ru_RU.mo differ diff --git a/src/retailcrm.php b/src/retailcrm.php index 63a0298..0a5b3ba 100644 --- a/src/retailcrm.php +++ b/src/retailcrm.php @@ -121,6 +121,7 @@ if (!class_exists( 'WC_Integration_Retailcrm')) : require_once(self::checkCustomFile('include/icml/class-wc-retailcrm-icml-writer.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-orders.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-cart.php')); + require_once(self::checkCustomFile('include/class-wc-retailcrm-loyalty.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-customers.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-inventories.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-history.php')); @@ -132,6 +133,7 @@ if (!class_exists( 'WC_Integration_Retailcrm')) : require_once(self::checkCustomFile('include/validators/url-validator/class-wc-retailcrm-url-constraint.php')); require_once(self::checkCustomFile('include/validators/url-validator/class-wc-retailcrm-url-validator.php')); require_once(self::checkCustomFile('include/validators/class-wc-retailcrm-validator-exception.php')); + require_once(self::checkCustomFile('include/components/class-wc-retailcrm-loyalty-form.php')); } /** diff --git a/tests/datasets/data-loyalty-retailcrm.php b/tests/datasets/data-loyalty-retailcrm.php new file mode 100644 index 0000000..ef9044f --- /dev/null +++ b/tests/datasets/data-loyalty-retailcrm.php @@ -0,0 +1,76 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +class DataLoyaltyRetailCrm +{ + public static function getDataLoyalty() + { + return [ + [ + 'isSuccessful' => true, + 'body' => json_encode(['loyaltyAccounts' => []]), + 'expected' => 'id="loyaltyRegisterForm"' + ], + [ + 'isSuccessful' => true, + 'body' => json_encode( + [ + 'loyaltyAccounts' => [ + 0 => [ + 'active' => false, + 'customer' => [ + 'externalId' => 1 + ], + 'id' => 1 + ] + ] + ] + ), + 'expected' => 'id="loyaltyActivateForm"' + ], + [ + 'isSuccessful' => true, + 'body' => json_encode( + [ + 'loyaltyAccounts' => [ + 0 => [ + 'id' => 1, + 'level' => [ + 'name' => 'Test level', + 'privilegeSize' => 5, + 'privilegeSizePromo' => 3, + 'type' => 'bonus_converting' + ], + 'amount' => 1000, + 'cardNumber' => '12345', + 'activatedAt' => '2024-04-10 15:00:00', + 'nextLevelSum' => 15000, + 'loyalty' => [ + 'currency' => 'USD' + ], + 'customer' => [ + 'externalId' => 1 + ], + 'active' => true + ] + ] + ] + ), + 'expected' => 'Ordinary products: accrual of 1 bonus for each' + ], + ]; + } +} diff --git a/tests/helpers/class-wc-retailcrm-test-case-helper.php b/tests/helpers/class-wc-retailcrm-test-case-helper.php index 2c09d1c..3ce02f1 100644 --- a/tests/helpers/class-wc-retailcrm-test-case-helper.php +++ b/tests/helpers/class-wc-retailcrm-test-case-helper.php @@ -80,7 +80,10 @@ class WC_Retailcrm_Test_Case_Helper extends WC_Unit_Test_Case 'product_description' => 'full', 'stores_for_uploading' => ['woocommerce', 'main'], 'woo_coupon_apply_field' => 'testField', - 'icml_unload_services' => 'yes' + 'icml_unload_services' => 'yes', + 'loyalty' => 'yes', + 'loyalty_terms' => 'Test terms', + 'loyalty_personal' => 'Test privacy' ]; update_option(WC_Retailcrm_Base::$option_key, $options); diff --git a/tests/test-wc-retailcrm-base.php b/tests/test-wc-retailcrm-base.php index aa280df..c24c3c9 100644 --- a/tests/test-wc-retailcrm-base.php +++ b/tests/test-wc-retailcrm-base.php @@ -132,6 +132,11 @@ class WC_Retailcrm_Base_Test extends WC_Retailcrm_Test_Case_Helper $this->assertArrayHasKey('bind_by_sku', $this->baseRetailcrm->form_fields); $this->assertArrayHasKey('update_number', $this->baseRetailcrm->form_fields); $this->assertArrayHasKey('product_description', $this->baseRetailcrm->form_fields); + + //loyalty + $this->assertArrayHasKey('loyalty', $this->baseRetailcrm->form_fields); + $this->assertArrayHasKey('loyalty_terms', $this->baseRetailcrm->form_fields); + $this->assertArrayHasKey('loyalty_personal', $this->baseRetailcrm->form_fields); } public function test_retailcrm_form_fields_value() diff --git a/tests/test-wc-retailcrm-loyalty.php b/tests/test-wc-retailcrm-loyalty.php new file mode 100644 index 0000000..9e6f653 --- /dev/null +++ b/tests/test-wc-retailcrm-loyalty.php @@ -0,0 +1,92 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ +class WC_Retailcrm_Loyalty_Test extends WC_Retailcrm_Test_Case_Helper +{ + protected $responseMock; + protected $apiMock; + + /** @var \WC_Retailcrm_Loyalty */ + protected $loyalty; + + public function setUp() + { + $this->responseMock = $this->getMockBuilder('\WC_Retailcrm_Response_Helper') + ->disableOriginalConstructor() + ->setMethods(['isSuccessful']) + ->getMock() + ; + + $this->responseMock->setResponse(['success' => true]); + $this->setMockResponse($this->responseMock, 'isSuccessful', true); + + $this->apiMock = $this->getMockBuilder('\WC_Retailcrm_Client_V5') + ->disableOriginalConstructor() + ->setMethods(['customersGet', 'getLoyaltyAccountList', 'createLoyaltyAccount', 'activateLoyaltyAccount']) + ->getMock() + ; + + $this->setMockResponse($this->apiMock, 'customersGet', ['customer' => ['id' => 1]]); + $this->setMockResponse($this->apiMock, 'createLoyaltyAccount', $this->responseMock); + $this->setMockResponse($this->apiMock, 'activateLoyaltyAccount', $this->responseMock); + + $this->loyalty = new WC_Retailcrm_Loyalty($this->apiMock, []); + } + + /** + * @dataProvider responseLoyalty + */ + public function testGetForm($isSuccessful, $body, $expected) + { + $response = new WC_Retailcrm_Response($isSuccessful ? 200 : 400, $body); + + $this->setMockResponse($this->apiMock, 'getLoyaltyAccountList', $response); + $this->loyalty = new WC_Retailcrm_Loyalty($this->apiMock, []); + + $result = $this->loyalty->getForm(1); + + if (isset($result['form'])) { + $this->assertTrue((bool) stripos($result['form'], $expected)); + } + } + + public function testRegistrationLoyalty() + { + $result = $this->loyalty->registerCustomer(1, '89999999999', 'test'); + + $this->assertTrue($result); + } + + public function testActivateLoyalty() + { + $result = $this->loyalty->activateLoyaltyCustomer(1); + + $this->assertTrue($result); + } + + public function responseLoyalty() + { + return DataLoyaltyRetailCrm::getDataLoyalty(); + } +}