Implement new WooCommerce dynamic segment based on the number of orders

[MAILPOET-3228]
This commit is contained in:
Rodrigo Primo
2021-04-13 14:22:33 -03:00
committed by Veljko V
parent 0153e63db0
commit 4f3738b0b1
11 changed files with 195 additions and 16 deletions

View File

@ -10,13 +10,17 @@ import {
WooCommerceFormItem,
} from '../types';
import { SegmentFormData } from '../segment_form_data';
import { Grid } from '../../../common/grid';
import Input from '../../../common/form/input/input';
export const WooCommerceOptions = [
{ value: 'numberOfOrders', label: MailPoet.I18n.t('wooNumberOfOrders'), group: SegmentTypes.WooCommerce },
{ value: 'purchasedCategory', label: MailPoet.I18n.t('wooPurchasedCategory'), group: SegmentTypes.WooCommerce },
{ value: 'purchasedProduct', label: MailPoet.I18n.t('wooPurchasedProduct'), group: SegmentTypes.WooCommerce },
];
enum WooCommerceActionTypes {
NUMBER_OF_ORDERS = 'numberOfOrders',
PURCHASED_CATEGORY = 'purchasedCategory',
PURCHASED_PRODUCT = 'purchasedProduct',
}
@ -35,6 +39,9 @@ export function validateWooCommerce(formItems: WooCommerceFormItem): boolean {
if (formItems.action === 'purchasedProduct' && !formItems.product_id) {
return false;
}
if (formItems.action === 'numberOfOrders' && (!formItems.number_of_orders_count || !formItems.number_of_orders_days || !formItems.number_of_orders_type)) {
return false;
}
return true;
}
@ -54,8 +61,16 @@ export const WooCommerceFields: React.FunctionComponent<Props> = ({ onChange, it
label: product.name,
}));
const numberOfOrdersTypeOptions = [
{ value: '=', label: 'equals' },
{ value: '>', label: 'more than' },
{ value: '<', label: 'less than' },
];
let optionFields;
if (item.action === WooCommerceActionTypes.PURCHASED_PRODUCT) {
return (
optionFields = (
<Select
isFullWidth
placeholder={MailPoet.I18n.t('selectWooPurchasedProduct')}
@ -68,9 +83,8 @@ export const WooCommerceFields: React.FunctionComponent<Props> = ({ onChange, it
automationId="select-segment-product"
/>
);
}
return (
} else if (item.action === WooCommerceActionTypes.PURCHASED_CATEGORY) {
optionFields = (
<Select
isFullWidth
placeholder={MailPoet.I18n.t('selectWooPurchasedCategory')}
@ -83,4 +97,51 @@ export const WooCommerceFields: React.FunctionComponent<Props> = ({ onChange, it
automationId="select-segment-category"
/>
);
} else if (item.action === WooCommerceActionTypes.NUMBER_OF_ORDERS) {
if (!item.number_of_orders_type) {
item.number_of_orders_type = '=';
}
optionFields = (
<div>
<Grid.CenteredRow className="mailpoet-form-field">
<Select
options={numberOfOrdersTypeOptions}
value={find(['value', item.number_of_orders_type], numberOfOrdersTypeOptions)}
onChange={(option: SelectOption): void => compose([
onChange,
assign(item),
])({ number_of_orders_type: option.value })}
/>
<Input
type="number"
min={0}
value={item.number_of_orders_count || ''}
placeholder="count"
onChange={(event): void => compose([
onChange,
assign(item),
])({ number_of_orders_count: event.target.value })}
/>
<div>orders</div>
</Grid.CenteredRow>
<Grid.CenteredRow className="mailpoet-form-field">
<div>in the last</div>
<Input
type="number"
min={1}
value={item.number_of_orders_days || ''}
placeholder="days"
onChange={(event): void => compose([
onChange,
assign(item),
])({ number_of_orders_days: event.target.value })}
/>
<div>days</div>
</Grid.CenteredRow>
</div>
);
}
return optionFields;
};

View File

@ -26,6 +26,9 @@ const allowedItemKeys: string[] = [
'days',
'opens',
'operator',
'number_of_orders_type',
'number_of_orders_count',
'number_of_orders_days',
];
function loadCount(formItem: AnyFormItem): Promise<Result | void> {

View File

@ -35,6 +35,9 @@ export interface WooCommerceFormItem extends FormItem {
action?: string;
category_id?: string;
product_id?: string;
number_of_orders_type?: string;
number_of_orders_count?: number;
number_of_orders_days?: number;
}
export interface EmailFormItem extends FormItem {

View File

@ -137,6 +137,8 @@ class DynamicSegments extends APIEndpoint {
return WPFunctions::get()->__('Please select category.', 'mailpoet');
case InvalidFilterException::MISSING_VALUE:
return WPFunctions::get()->__('Please fill all required values.', 'mailpoet');
case InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS:
return WPFunctions::get()->__('Please select a type for the comparison, a number of orders and a number of days.', 'mailpoet');
default:
return WPFunctions::get()->__('An error occurred while saving data.', 'mailpoet');
}

View File

@ -271,8 +271,9 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailAction::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\SegmentSaveController::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\FilterDataMapper::class)->setPublic(true);
// Services

View File

@ -15,5 +15,6 @@ class InvalidFilterException extends InvalidStateException {
const MISSING_PRODUCT_ID = 7;
const INVALID_EMAIL_ACTION = 8;
const MISSING_VALUE = 9;
const MISSING_NUMBER_OF_ORDERS_FIELDS = 10;
};

View File

@ -7,6 +7,7 @@ use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
class FilterDataMapper {
@ -87,6 +88,13 @@ class FilterDataMapper {
} elseif ($data['action'] === WooCommerceProduct::ACTION_PRODUCT) {
if (!isset($data['product_id'])) throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
$filterData['product_id'] = $data['product_id'];
} elseif ($data['action'] === WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS) {
if (!isset($data['number_of_orders_type']) || !isset($data['number_of_orders_count']) || !isset($data['number_of_orders_days'])) {
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS);
}
$filterData['number_of_orders_type'] = $data['number_of_orders_type'];
$filterData['number_of_orders_count'] = $data['number_of_orders_count'];
$filterData['number_of_orders_days'] = $data['number_of_orders_days'];
} else {
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
}

View File

@ -11,6 +11,7 @@ use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
use MailPoet\Segments\DynamicSegments\Filters\UserRole;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
use MailPoet\Segments\SegmentDependencyValidator;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
@ -29,6 +30,9 @@ class FilterHandler {
/** @var WooCommerceCategory */
private $wooCommerceCategory;
/** @var WooCommerceNumberOfOrders */
private $wooCommerceNumberOfOrders;
/** @var EntityManager */
private $entityManager;
@ -45,12 +49,14 @@ class FilterHandler {
WooCommerceProduct $wooCommerceProduct,
WooCommerceCategory $wooCommerceCategory,
EmailOpensAbsoluteCountAction $emailOpensAbsoluteCount,
SegmentDependencyValidator $segmentDependencyValidator
SegmentDependencyValidator $segmentDependencyValidator,
WooCommerceNumberOfOrders $wooCommerceNumberOfOrders
) {
$this->emailAction = $emailAction;
$this->userRole = $userRole;
$this->wooCommerceProduct = $wooCommerceProduct;
$this->wooCommerceCategory = $wooCommerceCategory;
$this->wooCommerceNumberOfOrders = $wooCommerceNumberOfOrders;
$this->entityManager = $entityManager;
$this->segmentDependencyValidator = $segmentDependencyValidator;
$this->emailOpensAbsoluteCount = $emailOpensAbsoluteCount;
@ -127,6 +133,8 @@ class FilterHandler {
$action = $filterData->getParam('action');
if ($action === WooCommerceProduct::ACTION_PRODUCT) {
return $this->wooCommerceProduct->apply($queryBuilder, $filter);
} elseif ($action === WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS) {
return $this->wooCommerceNumberOfOrders->apply($queryBuilder, $filter);
}
return $this->wooCommerceCategory->apply($queryBuilder, $filter);
default:

View File

@ -0,0 +1,59 @@
<?php
namespace MailPoet\Segments\DynamicSegments\Filters;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class WooCommerceNumberOfOrders implements Filter {
const ACTION_NUMBER_OF_ORDERS = 'numberOfOrders';
/** @var EntityManager */
private $entityManager;
public function __construct(EntityManager $entityManager) {
$this->entityManager = $entityManager;
}
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
global $wpdb;
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$filterData = $filter->getFilterData();
$type = $filterData->getParam('number_of_orders_type');
$count = $filterData->getParam('number_of_orders_count');
$days = $filterData->getParam('number_of_orders_days');
$date = Carbon::now()->subDays($days);
$queryBuilder->innerJoin(
$subscribersTable,
$wpdb->postmeta,
'postmeta',
"postmeta.meta_key = '_customer_user' AND $subscribersTable.wp_user_id=postmeta.meta_value"
)->leftJoin(
'postmeta',
$wpdb->posts,
'posts',
'posts.ID = postmeta.post_id AND posts.post_date >= :date AND postmeta.post_id NOT IN ( SELECT id FROM ' . $wpdb->posts . ' as p WHERE p.post_status IN ("wc-cancelled", "wc-failed"))'
)->setParameter(
'date', $date->toDateTimeString()
)->groupBy(
'inner_subscriber_id'
);
if ($type === '=') {
$queryBuilder->having("COUNT(posts.ID) = :count");
} elseif ($type === '>') {
$queryBuilder->having("COUNT(posts.ID) > :count");
} elseif ($type === '<') {
$queryBuilder->having("COUNT(posts.ID) < :count");
}
$queryBuilder->setParameter('count', $count);
return $queryBuilder;
}
}

View File

@ -7,6 +7,7 @@ use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
class FilterDataMapperTest extends \MailPoetUnitTest {
@ -226,4 +227,35 @@ class FilterDataMapperTest extends \MailPoetUnitTest {
$this->expectException(InvalidFilterException::class);
$this->mapper->map($data);
}
public function testItMapsWooCommerceNumberOfOrders() {
$data = [
'segmentType' => DynamicSegmentFilterData::TYPE_WOOCOMMERCE,
'action' => WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS,
'number_of_orders_type' => '=',
'number_of_orders_count' => 2,
'number_of_orders_days' => 1,
'some_mess' => 'mess',
];
$filter = $this->mapper->map($data);
unset($data['some_mess']);
$expectedResult = $data;
expect($filter)->isInstanceOf(DynamicSegmentFilterData::class);
expect($filter->getFilterType())->equals(DynamicSegmentFilterData::TYPE_WOOCOMMERCE);
expect($filter->getData())->equals($expectedResult);
}
public function testItRaisesExceptionWhenMappingWooCommerceNumberOfOrders() {
$this->expectException(InvalidFilterException::class);
$this->expectExceptionMessage('Missing required fields');
$this->expectExceptionCode(InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS);
$this->mapper->map([
'segmentType' => DynamicSegmentFilterData::TYPE_WOOCOMMERCE,
'action' => WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS,
]);
}
}

View File

@ -157,6 +157,7 @@
'multipleDynamicSegmentsDeleted': __('%$1d segments were permanently deleted.'),
'oneDynamicSegmentDeleted': __('1 segment was permanently deleted.'),
'wooNumberOfOrders': __('# of orders'),
'wooPurchasedCategory': __('purchased in this category'),
'wooPurchasedProduct': __('purchased this product'),
'selectWooPurchasedCategory': __('Search category'),