1
0
mirror of synced 2025-02-21 01:13:13 +03:00

Added functionality of abandoned carts

This commit is contained in:
Uryvskiy Dima 2023-03-16 23:08:59 +03:00 committed by Alex Lushpai
parent aed62d49f9
commit d3f298645d
9 changed files with 275 additions and 34 deletions

View File

@ -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

View File

@ -1 +1 @@
4.5.4
4.6.0

View File

@ -2,4 +2,44 @@
В версии 4.6.0 добавлен функционал выгрузки брошенных корзин.
Для активации необходимо включить опцию ***Выгружать брошенные корзины***
Для активации необходимо включить опцию ***Выгружать брошенные корзины***
### Брошенные корзины
Брошенная корзина - клиент заходит на сайт, добавляет/удаляет товары в корзине, а затем завершает визит без оформления заказа.
> Важно:
> * Корзины выгружаются только для зарегестрированных клиентов;
> * Для корректной работы корзин, один 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
<?php
add_filter('retailcrm_process_cart', 'process_crm_cart');
function process_crm_cart($crmCart, $cartItems)
{
$crmCart['updatedAt'] = null;
return $crmCart;
}
```
/
**Возможные API ошибки:**
* WC_Retailcrm_Client_V5::cartGet : Error: [HTTP-code 404] - корзина не найдена в CRM;

View File

@ -30,6 +30,9 @@ if (!class_exists('WC_Retailcrm_Base')) {
/** @var WC_Retailcrm_Uploader */
protected $uploader;
/** @var WC_Retailcrm_Cart */
protected $cart;
/**
* Init and hook in the integration.
*
@ -103,6 +106,15 @@ if (!class_exists('WC_Retailcrm_Base')) {
add_action('woocommerce_update_order', [$this, 'update_order'], 11, 1);
}
if ($this->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
*

View File

@ -0,0 +1,99 @@
<?php
if (!class_exists('WC_Retailcrm_Carts')) :
/**
* PHP version 7.0
*
* Class WC_Retailcrm_Cart - Allows transfer data carts with CMS.
*
* @category Integration
* @author RetailCRM <integration@retailcrm.ru>
* @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;

View File

@ -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

View File

@ -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'));

View File

@ -16,7 +16,7 @@
*
* @link https://wordpress.org/plugins/woo-retailcrm/
*
* @version 4.5.4
* @version 4.6.0
*
* @package RetailCRM
*/

View File

@ -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));
}
}