diff --git a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce.tsx b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce.tsx index b11116a937..cb440cf382 100644 --- a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce.tsx +++ b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce.tsx @@ -73,6 +73,14 @@ export function validateWooCommerce(formItems: WooCommerceFormItem): boolean { ) { return false; } + if ( + formItems.action === WooCommerceActionTypes.SINGLE_ORDER_VALUE && + (!formItems.single_order_value_amount || + !formItems.single_order_value_days || + !formItems.single_order_value_type) + ) { + return false; + } return true; } @@ -131,6 +139,12 @@ export const WooCommerceFields: FunctionComponent = ({ ) { void updateSegmentFilter({ number_of_orders_type: '=' }, filterIndex); } + if ( + segment.single_order_value_type === undefined && + segment.action === WooCommerceActionTypes.SINGLE_ORDER_VALUE + ) { + void updateSegmentFilter({ single_order_value_type: '>' }, filterIndex); + } if ( segment.total_spent_type === undefined && segment.action === WooCommerceActionTypes.TOTAL_SPENT @@ -354,7 +368,7 @@ export const WooCommerceFields: FunctionComponent = ({ min={0} step={0.01} value={segment.total_spent_amount || ''} - placeholder={MailPoet.I18n.t('wooTotalSpentAmount')} + placeholder={MailPoet.I18n.t('wooSpentAmount')} onChange={(e): void => { void updateSegmentFilterFromEvent( 'total_spent_amount', @@ -385,6 +399,66 @@ export const WooCommerceFields: FunctionComponent = ({ ); + } else if (segment.action === WooCommerceActionTypes.SINGLE_ORDER_VALUE) { + optionFields = ( + <> + + + { + void updateSegmentFilterFromEvent( + 'single_order_value_amount', + filterIndex, + e, + ); + }} + /> +
{wooCurrencySymbol}
+
+ +
{MailPoet.I18n.t('inTheLast')}
+ { + void updateSegmentFilterFromEvent( + 'single_order_value_days', + filterIndex, + e, + ); + }} + /> +
{MailPoet.I18n.t('days')}
+
+ + ); } else if (segment.action === WooCommerceActionTypes.CUSTOMER_IN_COUNTRY) { optionFields = ( <> diff --git a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce_options.ts b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce_options.ts index 4c6f57a50d..8afc4684bf 100644 --- a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce_options.ts +++ b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/woocommerce_options.ts @@ -8,6 +8,7 @@ export enum WooCommerceActionTypes { PURCHASED_PRODUCT = 'purchasedProduct', TOTAL_SPENT = 'totalSpent', CUSTOMER_IN_COUNTRY = 'customerInCountry', + SINGLE_ORDER_VALUE = 'singleOrderValue', } export const WooCommerceOptions = [ @@ -31,6 +32,11 @@ export const WooCommerceOptions = [ label: MailPoet.I18n.t('wooPurchasedProduct'), group: SegmentTypes.WooCommerce, }, + { + value: WooCommerceActionTypes.SINGLE_ORDER_VALUE, + label: MailPoet.I18n.t('wooSingleOrderValue'), + group: SegmentTypes.WooCommerce, + }, { value: WooCommerceActionTypes.TOTAL_SPENT, label: MailPoet.I18n.t('wooTotalSpent'), diff --git a/mailpoet/assets/js/src/segments/dynamic/types.ts b/mailpoet/assets/js/src/segments/dynamic/types.ts index 7c0293ffad..b9610a8cec 100644 --- a/mailpoet/assets/js/src/segments/dynamic/types.ts +++ b/mailpoet/assets/js/src/segments/dynamic/types.ts @@ -83,6 +83,9 @@ export interface WooCommerceFormItem extends FormItem { total_spent_amount?: number; total_spent_days?: number; country_code?: string[]; + single_order_value_type?: string; + single_order_value_amount?: number; + single_order_value_days?: number; } export interface WooCommerceMembershipFormItem extends FormItem { diff --git a/mailpoet/lib/API/JSON/v1/DynamicSegments.php b/mailpoet/lib/API/JSON/v1/DynamicSegments.php index 288ad58a73..22e4e9866e 100644 --- a/mailpoet/lib/API/JSON/v1/DynamicSegments.php +++ b/mailpoet/lib/API/JSON/v1/DynamicSegments.php @@ -146,6 +146,7 @@ class DynamicSegments extends APIEndpoint { case InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS: return __('Please select a type for the comparison, a number of orders and a number of days.', 'mailpoet'); case InvalidFilterException::MISSING_TOTAL_SPENT_FIELDS: + case InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS: return __('Please select a type for the comparison, an amount and a number of days.', 'mailpoet'); case InvalidFilterException::MISSING_FILTER: return __('Please add at least one condition for filtering.', 'mailpoet'); diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index 52f741a317..ee3201f2f9 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -398,6 +398,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership::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\Filters\WooCommerceSingleOrderValue::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\SegmentSaveController::class)->setPublic(true); diff --git a/mailpoet/lib/Segments/DynamicSegments/Exceptions/InvalidFilterException.php b/mailpoet/lib/Segments/DynamicSegments/Exceptions/InvalidFilterException.php index d0b247a2c9..a964c0e722 100644 --- a/mailpoet/lib/Segments/DynamicSegments/Exceptions/InvalidFilterException.php +++ b/mailpoet/lib/Segments/DynamicSegments/Exceptions/InvalidFilterException.php @@ -21,4 +21,5 @@ class InvalidFilterException extends InvalidStateException { const MISSING_FILTER = 14; const MISSING_OPERATOR = 15; const MISSING_PLAN_ID = 16; + const MISSING_SINGLE_ORDER_VALUE_FIELDS = 17; }; diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php index 6a512f719b..cef53d8b2d 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php @@ -17,6 +17,7 @@ use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent; use MailPoet\WP\Functions as WPFunctions; @@ -250,6 +251,17 @@ class FilterDataMapper { $filterData['total_spent_type'] = $data['total_spent_type']; $filterData['total_spent_amount'] = $data['total_spent_amount']; $filterData['total_spent_days'] = $data['total_spent_days']; + } elseif ($data['action'] === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) { + if ( + !isset($data['single_order_value_type']) + || !isset($data['single_order_value_amount']) || $data['single_order_value_amount'] < 0 + || !isset($data['single_order_value_days']) || $data['single_order_value_days'] < 1 + ) { + throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS); + } + $filterData['single_order_value_type'] = $data['single_order_value_type']; + $filterData['single_order_value_amount'] = $data['single_order_value_amount']; + $filterData['single_order_value_days'] = $data['single_order_value_days']; } else { throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION); } diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php index 75b862f7b0..aaf098025e 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php @@ -20,6 +20,7 @@ use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent; @@ -42,6 +43,9 @@ class FilterFactory { /** @var WooCommerceNumberOfOrders */ private $wooCommerceNumberOfOrders; + /** @var WooCommerceSingleOrderValue */ + private $wooCommerceSingleOrderValue; + /** @var WooCommerceTotalSpent */ private $wooCommerceTotalSpent; @@ -88,7 +92,8 @@ class FilterFactory { SubscriberSubscribedDate $subscriberSubscribedDate, SubscriberScore $subscriberScore, SubscriberTag $subscriberTag, - SubscriberSegment $subscriberSegment + SubscriberSegment $subscriberSegment, + WooCommerceSingleOrderValue $wooCommerceSingleOrderValue ) { $this->emailAction = $emailAction; $this->userRole = $userRole; @@ -106,6 +111,7 @@ class FilterFactory { $this->mailPoetCustomFields = $mailPoetCustomFields; $this->subscriberSegment = $subscriberSegment; $this->emailActionClickAny = $emailActionClickAny; + $this->wooCommerceSingleOrderValue = $wooCommerceSingleOrderValue; } public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter { @@ -182,6 +188,8 @@ class FilterFactory { return $this->wooCommerceTotalSpent; } elseif ($action === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) { return $this->wooCommerceCountry; + } elseif ($action === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) { + return $this->wooCommerceSingleOrderValue; } return $this->wooCommerceCategory; } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceSingleOrderValue.php b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceSingleOrderValue.php new file mode 100644 index 0000000000..62b63dc7e9 --- /dev/null +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceSingleOrderValue.php @@ -0,0 +1,85 @@ +entityManager = $entityManager; + $this->collationChecker = $collationChecker; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + global $wpdb; + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $filterData = $filter->getFilterData(); + $type = $filterData->getParam('single_order_value_type'); + $amount = $filterData->getParam('single_order_value_amount'); + $days = $filterData->getParam('single_order_value_days'); + + if (!is_string($days)) { + $days = '1'; // Default to last day + } + + $date = Carbon::now()->subDays((int)$days); + $parameterSuffix = $filter->getId() ?? Security::generateRandomString(); + $collation = $this->collationChecker->getCollateIfNeeded( + $subscribersTable, + 'email', + $wpdb->prefix . 'wc_customer_lookup', + 'email' + ); + + $queryBuilder->innerJoin( + $subscribersTable, + $wpdb->prefix . 'wc_customer_lookup', + 'customer', + "$subscribersTable.email = customer.email $collation" + )->leftJoin( + 'customer', + $wpdb->prefix . 'wc_order_stats', + 'orderStats', + 'customer.customer_id = orderStats.customer_id AND orderStats.date_created >= :date' . $parameterSuffix + )->andWhere( + 'orderStats.status NOT IN ("wc-cancelled", "wc-failed")' + )->setParameter( + 'date' . $parameterSuffix, $date->toDateTimeString() + ); + + if ($type === '=') { + $queryBuilder->andWhere('orderStats.total_sales = :amount' . $parameterSuffix); + } elseif ($type === '!=') { + $queryBuilder->andWhere('orderStats.total_sales != :amount' . $parameterSuffix); + } elseif ($type === '>') { + $queryBuilder->andWhere('orderStats.total_sales > :amount' . $parameterSuffix); + } elseif ($type === '>=') { + $queryBuilder->andWhere('orderStats.total_sales >= :amount' . $parameterSuffix); + } elseif ($type === '<') { + $queryBuilder->andWhere('orderStats.total_sales < :amount' . $parameterSuffix); + } elseif ($type === '<=') { + $queryBuilder->andWhere('orderStats.total_sales <= :amount' . $parameterSuffix); + } + + $queryBuilder->setParameter('amount' . $parameterSuffix, $amount); + + return $queryBuilder; + } +} diff --git a/mailpoet/views/segments.html b/mailpoet/views/segments.html index d3e68d52a1..78f38d64df 100644 --- a/mailpoet/views/segments.html +++ b/mailpoet/views/segments.html @@ -203,7 +203,9 @@ 'wooNumberOfOrders': __('# of orders'), 'moreThan': __('more than'), + 'moreThanOrEqual': __('more than or equal'), 'lessThan': __('less than'), + 'lessThanOrEqual': __('less than or equal'), 'wooNumberOfOrdersCount': __('count'), 'daysPlaceholder': __('days'), 'days': _x('days', 'Appears together with `inTheLast` when creating a new WooCommerce segment based on the number of orders.'), @@ -213,7 +215,8 @@ 'wooPurchasedProduct': __('purchased product'), 'wooTotalSpent': __('total spent'), 'wooCustomerInCountry': __('is in country'), - 'wooTotalSpentAmount': __('amount'), + 'wooSingleOrderValue': __('single order value'), + 'wooSpentAmount': __('amount'), 'selectWooPurchasedCategory': __('Search categories'), 'selectWooPurchasedProduct': __('Search products'), 'selectWooCountry': __('Search countries'),