From d3f298645d286f34454d54ee5251cd94fc1c0e87 Mon Sep 17 00:00:00 2001 From: Uryvskiy Dima Date: Thu, 16 Mar 2023 23:08:59 +0300 Subject: [PATCH] Added functionality of abandoned carts --- CHANGELOG.md | 3 + VERSION | 2 +- doc/1.Setup/Abandoned carts settings.md | 42 ++++++++++- src/include/class-wc-retailcrm-base.php | 77 +++++++++++++++++++ src/include/class-wc-retailcrm-cart.php | 99 +++++++++++++++++++++++++ src/readme.txt | 5 +- src/retailcrm.php | 3 +- src/uninstall.php | 2 +- tests/test-wc-retailcrm-cart.php | 76 +++++++++++-------- 9 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 src/include/class-wc-retailcrm-cart.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b592af3..55b1631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2022-03-17 4.6.0 +* Added functionality of abandoned carts + ## 2023-03-02 4.5.4 * Fix display payment methods diff --git a/VERSION b/VERSION index d01c9f6..28446a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.5.4 \ No newline at end of file +4.6.0 \ No newline at end of file diff --git a/doc/1.Setup/Abandoned carts settings.md b/doc/1.Setup/Abandoned carts settings.md index a772157..6e8540f 100644 --- a/doc/1.Setup/Abandoned carts settings.md +++ b/doc/1.Setup/Abandoned carts settings.md @@ -2,4 +2,44 @@ В версии 4.6.0 добавлен функционал выгрузки брошенных корзин. -Для активации необходимо включить опцию ***Выгружать брошенные корзины*** \ No newline at end of file +Для активации необходимо включить опцию ***Выгружать брошенные корзины*** + +### Брошенные корзины + +Брошенная корзина - клиент заходит на сайт, добавляет/удаляет товары в корзине, а затем завершает визит без оформления заказа. + +> Важно: +> * Корзины выгружаются только для зарегестрированных клиентов; +> * Для корректной работы корзин, один API ключ = один магизн в CRM; + +При разработке функционала, ориентировались на хуки корзины в WooCommerce: +* Хуки для метода **set_cart**: + * **woocommerce_add_to_cart** - добавление товара в корзину; + * **woocommerce_after_cart_item_quantity_update** - изменение кол-во товара в корзине; + * **woocommerce_cart_item_removed** - удаление товара с корзины; +* Хуки для метода **clear_cart**: + * **woocommerce_cart_emptied** - полная очистка корзины. Также срабатывает при создании заказа; + +Корзина создается в CRM, при первом добавлении товара. + +**Фильтры:** + +> retailcrm_process_cart - позволяет кастомизировать данные корзины. + +**Пример использования:** + +```php +get_option('abandoned_carts_enabled') === static::YES) { + $this->cart = new WC_Retailcrm_Cart($this->apiClient); + + add_action('woocommerce_add_to_cart', [$this, 'set_cart']); + add_action('woocommerce_after_cart_item_quantity_update', [$this, 'set_cart']); + add_action('woocommerce_cart_item_removed', [$this, 'set_cart']); + add_action('woocommerce_cart_emptied', [$this, 'clear_cart']); + } + // Deactivate hook add_action('retailcrm_deactivate', [$this, 'deactivate']); } @@ -385,6 +397,71 @@ if (!class_exists('WC_Retailcrm_Base')) { } } + /** + * Create and update cart in CRM + * + * @codeCoverageIgnore Check in another tests + * + * @return void + */ + public function set_cart() + { + global $woocommerce; + + $site = $this->apiClient->getSingleSiteForKey(); + $cartItems = $woocommerce->cart->get_cart(); + $customerId = $woocommerce->customer->get_id(); + + if (empty($site)) { + writeBaseLogs('Error with CRM credentials: need an valid apiKey assigned to one certain site'); + } elseif (empty($customerId)) { + writeBaseLogs('Abandoned carts work only for registered customers'); + } else { + $isCartExist = $this->cart->isCartExist($customerId, $site); + $isSuccessful = $this->cart->processCart($customerId, $cartItems, $site, $isCartExist); + + if ($isSuccessful) { + writeBaseLogs('Cart for customer ID: ' . $customerId . ' processed. Hook: ' . current_filter()); + } else { + writeBaseLogs('Cart for customer ID: ' . $customerId . ' not processed. Hook: ' . current_filter()); + } + } + } + + /** + * Clear the cart in CRM for 2 cases: + * 1. Delete all items from the basket; + * 2. Create an order, items from the cart are automatically deleted. + * + * The hook is called 3 times. + * + * @codeCoverageIgnore Check in another tests + * + * @return void + */ + public function clear_cart() + { + global $woocommerce; + + $site = $this->apiClient->getSingleSiteForKey(); + $customerId = $woocommerce->customer->get_id(); + + if (empty($site)) { + writeBaseLogs('Error with CRM credentials: need an valid apiKey assigned to one certain site'); + } elseif (empty($customerId)) { + writeBaseLogs('Abandoned carts work only for registered customers'); + } else { + $isCartExist = $this->cart->isCartExist($customerId, $site); + $isSuccessful = $this->cart->clearCart($customerId, $site, $isCartExist); + + if ($isSuccessful) { + writeBaseLogs('Cart for customer ID: ' . $customerId . ' cleared. Hook: ' . current_filter()); + } elseif ($isCartExist) { + writeBaseLogs('Cart for customer ID: ' . $customerId . ' not cleared. Hook: ' . current_filter()); + } + } + } + /** * Edit order in retailCRM * diff --git a/src/include/class-wc-retailcrm-cart.php b/src/include/class-wc-retailcrm-cart.php new file mode 100644 index 0000000..eea4235 --- /dev/null +++ b/src/include/class-wc-retailcrm-cart.php @@ -0,0 +1,99 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + class WC_Retailcrm_Cart + { + protected $apiClient; + protected $dateFormat; + + public function __construct($apiClient) + { + $this->apiClient = $apiClient; + $this->dateFormat = 'Y-m-d H:i:sP'; + } + + public function isCartExist($customerId, $site): bool + { + $getCart = $this->apiClient->cartGet($customerId, $site); + + return !empty($getCart['cart']['externalId']); + } + + public function processCart($customerId, $cartItems, $site, $isCartExist): bool + { + $isSuccessful = false; + + try { + $crmCart = [ + 'customer' => ['externalId' => $customerId], + 'clearAt' => null, + 'updatedAt' => date($this->dateFormat) + ]; + + // If new cart, need set createdAt and externalId + if (!$isCartExist) { + $crmCart['createdAt'] = date($this->dateFormat); + $crmCart['externalId'] = $customerId . uniqid('_', true); + } + + // If you delete one by one + if (empty($cartItems)) { + return $this->clearCart($customerId, $site, $isCartExist); + } + + foreach ($cartItems as $item) { + $product = $item['data']; + + $crmCart['items'][] = [ + 'offer' => ['externalId' => $product->get_id()], + 'quantity' => $item['quantity'], + 'createdAt' => $product->get_date_created()->date($this->dateFormat) ?? date($this->dateFormat), + 'updatedAt' => $product->get_date_modified()->date($this->dateFormat) ?? date($this->dateFormat), + 'price' => wc_get_price_including_tax($product), + ]; + } + + $crmCart = apply_filters( + 'retailcrm_process_cart', + WC_Retailcrm_Plugin::clearArray($crmCart), + $cartItems + ); + + $setResponse = $this->apiClient->cartSet($crmCart, $site); + $isSuccessful = $setResponse->isSuccessful() && !empty($setResponse['success']); + } catch (Throwable $exception) { + writeBaseLogs('Error process cart: ' . $exception->getMessage()); + } + + return $isSuccessful; + } + + public function clearCart($customerId, $site, $isCartExist): bool + { + $isSuccessful = false; + + try { + if ($isCartExist) { + $crmCart = ['customer' => ['externalId' => $customerId], 'clearedAt' => date($this->dateFormat)]; + $clearResponse = $this->apiClient->cartClear($crmCart, $site); + $isSuccessful = $clearResponse->isSuccessful() && !empty($clearResponse['success']); + } + } catch (Throwable $exception) { + writeBaseLogs('Error clear cart: ' . $exception->getMessage()); + } + + return $isSuccessful; + } + } +endif; diff --git a/src/readme.txt b/src/readme.txt index 6e16b07..1425873 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -5,7 +5,7 @@ Tags: Интеграция, Simla.com, simla Requires PHP: 7.0 Requires at least: 5.3 Tested up to: 6.0 -Stable tag: 4.5.4 +Stable tag: 4.6.0 License: GPLv1 or later License URI: http://www.gnu.org/licenses/gpl-1.0.html @@ -82,6 +82,9 @@ Asegúrate de tener una clave API específica para cada tienda. Las siguientes i == Changelog == += 4.6.0 = +* Added functionality of abandoned carts + = 4.5.4 = * Fix display payment methods diff --git a/src/retailcrm.php b/src/retailcrm.php index 0207c47..ea6411e 100644 --- a/src/retailcrm.php +++ b/src/retailcrm.php @@ -5,7 +5,7 @@ * Description: Integration plugin for WooCommerce & Simla.com * Author: RetailDriver LLC * Author URI: http://retailcrm.pro/ - * Version: 4.5.4 + * Version: 4.6.0 * Tested up to: 6.0 * WC requires at least: 5.4 * WC tested up to: 6.9 @@ -119,6 +119,7 @@ if (!class_exists( 'WC_Integration_Retailcrm')) : require_once(self::checkCustomFile('include/class-wc-retailcrm-icml.php')); 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-customers.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-inventories.php')); require_once(self::checkCustomFile('include/class-wc-retailcrm-history.php')); diff --git a/src/uninstall.php b/src/uninstall.php index fe81833..ef2728c 100644 --- a/src/uninstall.php +++ b/src/uninstall.php @@ -16,7 +16,7 @@ * * @link https://wordpress.org/plugins/woo-retailcrm/ * - * @version 4.5.4 + * @version 4.6.0 * * @package RetailCRM */ diff --git a/tests/test-wc-retailcrm-cart.php b/tests/test-wc-retailcrm-cart.php index 3648436..4749f7b 100644 --- a/tests/test-wc-retailcrm-cart.php +++ b/tests/test-wc-retailcrm-cart.php @@ -15,58 +15,76 @@ use datasets\DataCartRetailCrm; */ class WC_Retailcrm_Cart_Test extends WC_Retailcrm_Test_Case_Helper { - protected $apiClientMock; + protected $cart; + protected $apiMock; protected $responseMock; public function setUp() { $this->responseMock = $this->getMockBuilder('\WC_Retailcrm_Response_Helper') ->disableOriginalConstructor() - ->setMethods( - [ - 'isSuccessful', - 'offsetExists', - ] - ) + ->setMethods(['isSuccessful']) ->getMock(); - $this->responseMock->setResponse(['id' => 1]); - - $this->apiClientMock = $this->getMockBuilder('\WC_Retailcrm_Client_V5') + $this->apiMock = $this->getMockBuilder('\WC_Retailcrm_Client_V5') ->disableOriginalConstructor() - ->setMethods( - [ - 'cartGet', - 'cartSet', - 'cartClear', - ] - ) + ->setMethods(['cartGet', 'cartSet', 'cartClear']) ->getMock(); + $this->responseMock->setResponse(['success' => true, ]); $this->setMockResponse($this->responseMock, 'isSuccessful', true); - $this->setMockResponse($this->responseMock, 'offsetExists', true); - $this->setMockResponse($this->apiClientMock, 'cartSet', $this->responseMock); - $this->setMockResponse($this->apiClientMock, 'cartClear', $this->responseMock); - $this->setMockResponse($this->apiClientMock, 'cartGet',$this->responseMock); + $this->setMockResponse($this->apiMock, 'cartGet', ['cart' => ['externalId' => 1]]); + $this->setMockResponse($this->apiMock, 'cartSet', $this->responseMock); + $this->setMockResponse($this->apiMock, 'cartClear', $this->responseMock); + + $this->cart = new WC_Retailcrm_Cart($this->apiMock); } - public function testGetCart() + public function testApiGetCart() { $this->responseMock->setResponse(DataCartRetailCrm::dataGetCart()); - $response = $this->apiClientMock->cartGet(1, 'test-site'); - $this->assertNotEmpty($response->__get('cart')); - $this->assertTrue($response->__get('success')); + + $response = $this->apiMock->cartGet(1, 'test-site'); + + $this->assertNotEmpty($response['cart']); + $this->assertNotEmpty($response['cart']['externalId']); + $this->assertEquals(1, $response['cart']['externalId']); + } + + public function testApiSetCart() + { + $response = $this->apiMock->cartSet(DataCartRetailCrm::dataSetCart(), 'test-site'); + + $this->assertNotEmpty($response['success']); + $this->assertTrue($response['success']); + } + + public function testApiClearCart() + { + $response = $this->apiMock->cartClear(DataCartRetailCrm::dataClearCart(), 'test-site'); + + $this->assertNotEmpty($response['success']); + $this->assertTrue($response['success']); } public function testSetCart() { - $response = $this->apiClientMock->cartSet(DataCartRetailCrm::dataSetCart(), 'test-site'); - $this->assertEquals(1, $response->__get('id')); + $wcCart = new WC_Cart(); + $product = WC_Helper_Product::create_simple_product(); + $customerId = wc_create_new_customer('mail_test@mail.es', 'test'); + + $wcCart->add_to_cart($product->get_id(), 1, 0, [], []); + + $this->assertTrue($this->cart->processCart($customerId, $wcCart->get_cart(), 'woo', true)); + } + + public function testGetCart() + { + $this->assertTrue($this->cart->isCartExist(1, 'woo')); } public function testClearCart() { - $response = $this->apiClientMock->cartClear(DataCartRetailCrm::dataClearCart(), 'test-site'); - $this->assertEquals(1, $response->__get('id')); + $this->assertTrue($this->cart->clearCart(1, 'woo', true)); } }