Implement new WooCommerce dynamic segment based on the number of orders
[MAILPOET-3228]
This commit is contained in:
@ -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;
|
||||
};
|
||||
|
@ -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> {
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
Reference in New Issue
Block a user