From ec68818c1a3f96e04ed1131d58a9244c97ac9ab5 Mon Sep 17 00:00:00 2001 From: John Oleksowicz Date: Wed, 10 May 2023 12:23:09 -0500 Subject: [PATCH] Add filter for used payment method MAILPOET-4993 --- mailpoet/assets/css/src/generic/_helpers.scss | 4 + .../js/src/common/form/select/select.tsx | 3 + .../dynamic_segments_filters/woocommerce.tsx | 102 +++++++++++++ .../woocommerce_options.ts | 6 + .../segments/dynamic/store/initial_state.ts | 1 + .../src/segments/dynamic/store/selectors.ts | 3 + .../assets/js/src/segments/dynamic/types.ts | 9 ++ mailpoet/lib/AdminPages/Pages/Segments.php | 12 ++ mailpoet/lib/DI/ContainerConfigurator.php | 1 + .../DynamicSegments/FilterDataMapper.php | 14 ++ .../DynamicSegments/FilterFactory.php | 8 ++ .../DynamicSegments/Filters/FilterHelper.php | 2 +- .../Filters/WooCommerceUsedPaymentMethod.php | 134 ++++++++++++++++++ mailpoet/lib/WooCommerce/Helper.php | 4 + .../WooCommerceUsedPaymentMethodTest.php | 122 ++++++++++++++++ mailpoet/views/segments.html | 3 + 16 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethod.php create mode 100644 mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethodTest.php diff --git a/mailpoet/assets/css/src/generic/_helpers.scss b/mailpoet/assets/css/src/generic/_helpers.scss index f8096b7892..d9f5b08e5d 100644 --- a/mailpoet/assets/css/src/generic/_helpers.scss +++ b/mailpoet/assets/css/src/generic/_helpers.scss @@ -2,6 +2,10 @@ width: min-content; } +.mailpoet-max-content-width { + max-width: max-content; +} + .mailpoet-nowrap { white-space: nowrap; } diff --git a/mailpoet/assets/js/src/common/form/select/select.tsx b/mailpoet/assets/js/src/common/form/select/select.tsx index 63c5cb12ab..362a16b86a 100644 --- a/mailpoet/assets/js/src/common/form/select/select.tsx +++ b/mailpoet/assets/js/src/common/form/select/select.tsx @@ -6,6 +6,7 @@ type Props = SelectHTMLAttributes & { dimension?: 'small'; isFullWidth?: boolean; isMinWidth?: boolean; + isMaxContentWidth?: boolean; iconStart?: JSX.Element; automationId?: string; }; @@ -17,6 +18,7 @@ export const Select = forwardRef( dimension, isFullWidth, isMinWidth, + isMaxContentWidth, iconStart, automationId, ...attributes @@ -29,6 +31,7 @@ export const Select = forwardRef( 'mailpoet-disabled': attributes.disabled, 'mailpoet-full-width': isFullWidth, 'mailpoet-min-width': isMinWidth, + 'mailpoet-max-content-width': isMaxContentWidth, })} > {iconStart} 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 348feb70d7..5c3f6b0b12 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 @@ -14,6 +14,7 @@ import { WindowProducts, WindowWooCommerceCountries, WooCommerceFormItem, + WooPaymentMethod, } from '../types'; import { DateFields, validateDateField } from './date_fields'; import { storeName } from '../store'; @@ -87,6 +88,17 @@ export function validateWooCommerce(formItems: WooCommerceFormItem): boolean { ) { return false; } + if (formItems.action === WooCommerceActionTypes.USED_PAYMENT_METHOD) { + if ( + !formItems.payment_methods || + formItems.payment_methods.length < 1 || + !formItems.operator || + !formItems.used_payment_method_days || + parseInt(formItems.used_payment_method_days, 10) < 1 + ) { + return false; + } + } if (formItems.action === WooCommerceActionTypes.PURCHASE_DATE) { return validateDateField(formItems); } @@ -621,6 +633,95 @@ function CustomerInCountryFields({ filterIndex }: Props): JSX.Element { ); } +function UsedPaymentMethodFields({ filterIndex }: Props): JSX.Element { + const segment: WooCommerceFormItem = useSelect( + (select) => select(storeName).getSegmentFilter(filterIndex), + [filterIndex], + ); + const { updateSegmentFilter, updateSegmentFilterFromEvent } = + useDispatch(storeName); + const paymentMethods: WooPaymentMethod[] = useSelect( + (select) => select(storeName).getPaymentMethods(), + [], + ); + const paymentMethodOptions = paymentMethods.map((method) => ({ + value: method.id, + label: method.name, + })); + + useEffect(() => { + if ( + segment.operator !== AnyValueTypes.ANY && + segment.operator !== AnyValueTypes.ALL && + segment.operator !== AnyValueTypes.NONE + ) { + void updateSegmentFilter({ operator: AnyValueTypes.ANY }, filterIndex); + } + }, [updateSegmentFilter, segment, filterIndex]); + + return ( + <> + + + { + if (!segment.payment_methods) return undefined; + return segment.payment_methods.indexOf(option.value) !== -1; + }, paymentMethodOptions)} + onChange={(options: SelectOption[]): void => { + void updateSegmentFilter( + { + payment_methods: (options || []).map( + (x: SelectOption) => x.value, + ), + }, + filterIndex, + ); + }} + automationId="select-payment-methods" + /> + + +
{MailPoet.I18n.t('inTheLast')}
+ { + void updateSegmentFilterFromEvent( + 'used_payment_method_days', + filterIndex, + e, + ); + }} + /> +
{MailPoet.I18n.t('days')}
+
+ + ); +} + const componentsMap = { [WooCommerceActionTypes.CUSTOMER_IN_COUNTRY]: CustomerInCountryFields, [WooCommerceActionTypes.NUMBER_OF_ORDERS]: NumberOfOrdersFields, @@ -630,6 +731,7 @@ const componentsMap = { [WooCommerceActionTypes.SINGLE_ORDER_VALUE]: SingleOrderValueFields, [WooCommerceActionTypes.TOTAL_SPENT]: TotalSpentFields, [WooCommerceActionTypes.AVERAGE_SPENT]: AverageSpentFields, + [WooCommerceActionTypes.USED_PAYMENT_METHOD]: UsedPaymentMethodFields, }; export function WooCommerceFields({ filterIndex }: Props): JSX.Element { 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 eb4e74c999..34c6f448bc 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 @@ -11,6 +11,7 @@ export enum WooCommerceActionTypes { AVERAGE_SPENT = 'averageSpent', CUSTOMER_IN_COUNTRY = 'customerInCountry', SINGLE_ORDER_VALUE = 'singleOrderValue', + USED_PAYMENT_METHOD = 'usedPaymentMethod', } export const WooCommerceOptions = [ @@ -54,6 +55,11 @@ export const WooCommerceOptions = [ label: MailPoet.I18n.t('wooTotalSpent'), group: SegmentTypes.WooCommerce, }, + { + value: WooCommerceActionTypes.USED_PAYMENT_METHOD, + label: MailPoet.I18n.t('wooUsedPaymentMethod'), + group: SegmentTypes.WooCommerce, + }, ]; // WooCommerce Memberships diff --git a/mailpoet/assets/js/src/segments/dynamic/store/initial_state.ts b/mailpoet/assets/js/src/segments/dynamic/store/initial_state.ts index 3f0763ded1..b7befa8fa6 100644 --- a/mailpoet/assets/js/src/segments/dynamic/store/initial_state.ts +++ b/mailpoet/assets/js/src/segments/dynamic/store/initial_state.ts @@ -21,6 +21,7 @@ export const getInitialState = (): StateType => ({ canUseWooSubscriptions: window.mailpoet_can_use_woocommerce_subscriptions, wooCurrencySymbol: window.mailpoet_woocommerce_currency_symbol, wooCountries: window.mailpoet_woocommerce_countries, + wooPaymentMethods: window.mailpoet_woocommerce_payment_methods, customFieldsList: window.mailpoet_custom_fields, tags: window.mailpoet_tags, signupForms: window.mailpoet_signup_forms, diff --git a/mailpoet/assets/js/src/segments/dynamic/store/selectors.ts b/mailpoet/assets/js/src/segments/dynamic/store/selectors.ts index f533117898..51606fb602 100644 --- a/mailpoet/assets/js/src/segments/dynamic/store/selectors.ts +++ b/mailpoet/assets/js/src/segments/dynamic/store/selectors.ts @@ -17,6 +17,7 @@ import { WindowProducts, WindowSubscriptionProducts, WindowWooCommerceCountries, + WooPaymentMethod, } from '../types'; export const getProducts = (state: StateType): WindowProducts => state.products; @@ -49,6 +50,8 @@ export const getSubscriberCount = (state: StateType): SubscriberCount => export const getTags = (state: StateType): Tag[] => state.tags; export const getSignupForms = (state: StateType): SignupForm[] => state.signupForms; +export const getPaymentMethods = (state: StateType): WooPaymentMethod[] => + state.wooPaymentMethods; export const getSegmentFilter = ( state: StateType, index: number, diff --git a/mailpoet/assets/js/src/segments/dynamic/types.ts b/mailpoet/assets/js/src/segments/dynamic/types.ts index 7d7d52ab66..e7726effff 100644 --- a/mailpoet/assets/js/src/segments/dynamic/types.ts +++ b/mailpoet/assets/js/src/segments/dynamic/types.ts @@ -100,6 +100,8 @@ export interface WooCommerceFormItem extends FormItem { average_spent_type?: string; average_spent_amount?: string; average_spent_days?: string; + payment_methods?: string[]; + used_payment_method_days?: string; } export interface WooCommerceMembershipFormItem extends FormItem { @@ -204,6 +206,7 @@ export interface SegmentFormDataWindow extends Window { mailpoet_subscription_products: WindowSubscriptionProducts; mailpoet_product_categories: WindowProductCategories; mailpoet_woocommerce_countries: WindowWooCommerceCountries; + mailpoet_woocommerce_payment_methods: WooPaymentMethod[]; mailpoet_newsletters_list: WindowNewslettersList; mailpoet_custom_fields: WindowCustomFields; mailpoet_can_use_woocommerce_memberships: boolean; @@ -225,6 +228,7 @@ export interface StateType { canUseWooSubscriptions: boolean; wooCurrencySymbol: string; wooCountries: WindowWooCommerceCountries; + wooPaymentMethods: WooPaymentMethod[]; customFieldsList: WindowCustomFields; segment: Segment; subscriberCount: SubscriberCount; @@ -280,3 +284,8 @@ export type SignupForm = { id: string; name: string; }; + +export type WooPaymentMethod = { + id: string; + name: string; +}; diff --git a/mailpoet/lib/AdminPages/Pages/Segments.php b/mailpoet/lib/AdminPages/Pages/Segments.php index 844966f8d2..47cc880158 100644 --- a/mailpoet/lib/AdminPages/Pages/Segments.php +++ b/mailpoet/lib/AdminPages/Pages/Segments.php @@ -136,6 +136,18 @@ class Segments { 'name' => $form->getName(), ]; }, $this->formsRepository->findAll()); + $data['woocommerce_payment_methods'] = []; + if ($this->woocommerceHelper->isWooCommerceActive()) { + $allGateways = $this->woocommerceHelper->getPaymentGateways()->payment_gateways(); + $paymentMethods = []; + foreach ($allGateways as $gatewayId => $gateway) { + $paymentMethods[] = [ + 'id' => $gatewayId, + 'name' => $gateway->get_method_title(), + ]; + } + $data['woocommerce_payment_methods'] = $paymentMethods; + } $this->pageRenderer->displayPage('segments.html', $data); } diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index fe11749dfb..a769407128 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -428,6 +428,7 @@ class ContainerConfigurator implements IContainerConfigurator { $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\Filters\WooCommerceUsedPaymentMethod::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooFilterHelper::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\SegmentSaveController::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\FilterDataMapper::class)->setPublic(true); diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php index ee065a844b..c400e1f9da 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php @@ -25,6 +25,7 @@ use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod; use MailPoet\WP\Functions as WPFunctions; class FilterDataMapper { @@ -309,6 +310,19 @@ class FilterDataMapper { $filterData['average_spent_days'] = $data['average_spent_days']; $filterData['average_spent_amount'] = $data['average_spent_amount']; $filterData['average_spent_type'] = $data['average_spent_type']; + } elseif ($data['action'] === WooCommerceUsedPaymentMethod::ACTION) { + if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedPaymentMethod::VALID_OPERATORS, true)) { + throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR); + } + if (!isset($data['payment_methods']) || !is_array($data['payment_methods']) || empty($data['payment_methods'])) { + throw new InvalidFilterException('Missing payment gateways', InvalidFilterException::MISSING_VALUE); + } + if (!isset($data['used_payment_method_days']) || intval($data['used_payment_method_days']) < 1) { + throw new InvalidFilterException('Missing days', InvalidFilterException::MISSING_VALUE); + } + $filterData['operator'] = $data['operator']; + $filterData['payment_methods'] = $data['payment_methods']; + $filterData['used_payment_method_days'] = intval($data['used_payment_method_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 0dae3d5beb..36bc6a9a41 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php @@ -27,6 +27,7 @@ use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod; class FilterFactory { /** @var EmailAction */ @@ -92,6 +93,9 @@ class FilterFactory { /** @var SubscriberTextField */ private $subscriberTextField; + /** @var WooCommerceUsedPaymentMethod */ + private $wooCommerceUsedPaymentMethod; + public function __construct( EmailAction $emailAction, EmailActionClickAny $emailActionClickAny, @@ -113,6 +117,7 @@ class FilterFactory { SubscriberSubscribedViaForm $subscribedViaForm, WooCommerceSingleOrderValue $wooCommerceSingleOrderValue, WooCommerceAverageSpent $wooCommerceAverageSpent, + WooCommerceUsedPaymentMethod $wooCommerceUsedPaymentMethod, SubscriberTextField $subscriberTextField ) { $this->emailAction = $emailAction; @@ -136,6 +141,7 @@ class FilterFactory { $this->subscriberTextField = $subscriberTextField; $this->subscribedViaForm = $subscribedViaForm; $this->wooCommerceAverageSpent = $wooCommerceAverageSpent; + $this->wooCommerceUsedPaymentMethod = $wooCommerceUsedPaymentMethod; } public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter { @@ -223,6 +229,8 @@ class FilterFactory { return $this->wooCommercePurchaseDate; } elseif ($action === WooCommerceAverageSpent::ACTION) { return $this->wooCommerceAverageSpent; + } elseif ($action === WooCommerceUsedPaymentMethod::ACTION) { + return $this->wooCommerceUsedPaymentMethod; } return $this->wooCommerceCategory; } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php b/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php index c8fc96f58d..1f4258bfed 100644 --- a/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php @@ -26,7 +26,7 @@ class FilterHelper { return $this->entityManager ->getConnection() ->createQueryBuilder() - ->select('id') + ->select($this->getSubscribersTable() . '.id') ->from($this->getSubscribersTable()); } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethod.php b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethod.php new file mode 100644 index 0000000000..52c40b6604 --- /dev/null +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethod.php @@ -0,0 +1,134 @@ +wooFilterHelper = $wooFilterHelper; + $this->wooHelper = $wooHelper; + $this->filterHelper = $filterHelper; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + $filterData = $filter->getFilterData(); + $operator = $filterData->getParam('operator'); + $paymentMethods = $filterData->getParam('payment_methods'); + $days = $filterData->getParam('used_payment_method_days'); + + if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) { + throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR); + } + + if (!is_array($paymentMethods) || count($paymentMethods) < 1) { + throw new InvalidFilterException('Missing payment methods', InvalidFilterException::MISSING_VALUE); + } + + if (!is_int($days) || $days < 1) { + throw new InvalidFilterException('Missing days', InvalidFilterException::MISSING_VALUE); + } + + $includedStatuses = array_keys($this->wooHelper->getOrderStatuses()); + $failedKey = array_search('wc-failed', $includedStatuses, true); + if ($failedKey !== false) { + unset($includedStatuses[$failedKey]); + } + $date = Carbon::now()->subDays($days); + + switch ($operator) { + case DynamicSegmentFilterData::OPERATOR_ANY: + $this->applyForAnyOperator($queryBuilder, $includedStatuses, $paymentMethods, $date); + break; + case DynamicSegmentFilterData::OPERATOR_ALL: + $this->applyForAllOperator($queryBuilder, $includedStatuses, $paymentMethods, $date); + break; + case DynamicSegmentFilterData::OPERATOR_NONE: + $subQuery = $this->filterHelper->getNewSubscribersQueryBuilder(); + $this->applyForAnyOperator($subQuery, $includedStatuses, $paymentMethods, $date); + $subscribersTable = $this->filterHelper->getSubscribersTable(); + $queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery))); + break; + } + + return $queryBuilder; + } + + private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date): void { + if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) { + $this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date); + } else { + $this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date); + } + } + + private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date): void { + if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) { + $ordersAlias = $this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date); + $queryBuilder->groupBy('inner_subscriber_id') + ->having("COUNT(DISTINCT $ordersAlias.payment_method) = " . count($paymentMethods)); + } else { + $postmetaAlias = $this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date); + $queryBuilder->groupBy('inner_subscriber_id')->having("COUNT(DISTINCT $postmetaAlias.meta_value) = " . count($paymentMethods)); + } + } + + private function applyPostmetaOrderJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, string $postmetaAlias = 'postmeta'): string { + $dateParam = $this->filterHelper->getUniqueParameterName('date'); + $paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod'); + $paymentMethodMetaKeyParam = $this->filterHelper->getUniqueParameterName('paymentMethod'); + + $postMetaTable = $this->filterHelper->getPrefixedTable('postmeta'); + $orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses); + $queryBuilder + ->innerJoin($orderStatsAlias, $postMetaTable, $postmetaAlias, "$orderStatsAlias.order_id = $postmetaAlias.post_id") + ->andWhere("$orderStatsAlias.date_created >= :$dateParam") + ->andWhere("postmeta.meta_key = :$paymentMethodMetaKeyParam") + ->andWhere("postmeta.meta_value IN (:$paymentMethodParam)") + ->setParameter($paymentMethodMetaKeyParam, '_payment_method') + ->setParameter($dateParam, $date->toDateTimeString()) + ->setParameter($paymentMethodParam, $paymentMethods, Connection::PARAM_STR_ARRAY); + return $postmetaAlias; + } + + private function applyCustomOrderTableJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, string $ordersAlias = 'orders'): string { + $dateParam = $this->filterHelper->getUniqueParameterName('date'); + $paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod'); + $ordersTable = $this->wooHelper->getOrdersTableName(); + $orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses); + $queryBuilder + ->innerJoin($orderStatsAlias, $ordersTable, 'orders', "$orderStatsAlias.order_id = orders.id") + ->andWhere("$orderStatsAlias.date_created >= :$dateParam") + ->andWhere("$ordersAlias.payment_method IN (:$paymentMethodParam)") + ->setParameter($dateParam, $date->toDateTimeString()) + ->setParameter($paymentMethodParam, $paymentMethods, Connection::PARAM_STR_ARRAY); + return $ordersAlias; + } +} diff --git a/mailpoet/lib/WooCommerce/Helper.php b/mailpoet/lib/WooCommerce/Helper.php index 1a954fc7f5..56004f2005 100644 --- a/mailpoet/lib/WooCommerce/Helper.php +++ b/mailpoet/lib/WooCommerce/Helper.php @@ -223,4 +223,8 @@ class Helper { return $coupon ? $coupon->post_title : null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps } + + public function getPaymentGateways() { + return $this->WC()->payment_gateways(); + } } diff --git a/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethodTest.php b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethodTest.php new file mode 100644 index 0000000000..8ff59d824b --- /dev/null +++ b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceUsedPaymentMethodTest.php @@ -0,0 +1,122 @@ +filter = $this->diContainer->get(WooCommerceUsedPaymentMethod::class); + } + + public function testItWorksWithAnyOperator(): void { + $customerId1 = $this->tester->createCustomer('c1@e.com'); + $customerId2 = $this->tester->createCustomer('c2@e.com'); + $customerId3 = $this->tester->createCustomer('c3@e.com'); + + $this->createOrder($customerId1, Carbon::now(), 'paypal'); + $this->createOrder($customerId2, Carbon::now(), 'cheque'); + $this->createOrder($customerId3, Carbon::now(), 'cheque'); + $this->createOrder($customerId3, Carbon::now(), 'paypal'); + + $this->assertFilterReturnsEmails('any', ['paypal'], 1, ['c1@e.com', 'c3@e.com']); + $this->assertFilterReturnsEmails('any', ['cheque'], 1, ['c2@e.com', 'c3@e.com']); + $this->assertFilterReturnsEmails('any', ['doge'], 1000, []); + } + + public function testItWorksWithAllOperator(): void { + $customerId1 = $this->tester->createCustomer('c1@e.com'); + $this->createOrder($customerId1, Carbon::now(), 'paypal'); + $this->createOrder($customerId1, Carbon::now(), 'paypal'); + + $customerId2 = $this->tester->createCustomer('c2@e.com'); + $this->createOrder($customerId2, Carbon::now(), 'cheque'); + + $customerId3 = $this->tester->createCustomer('c3@e.com'); + $this->createOrder($customerId3, Carbon::now(), 'cheque'); + $this->createOrder($customerId3, Carbon::now(), 'paypal'); + + $this->assertfilterreturnsemails('all', ['paypal'], 1, ['c1@e.com', 'c3@e.com']); + $this->assertFilterReturnsEmails('all', ['cheque'], 1, ['c2@e.com', 'c3@e.com']); + $this->assertFilterReturnsEmails('all', ['cheque', 'paypal'], 1, ['c3@e.com']); + $this->assertFilterReturnsEmails('all', ['doge'], 1000, []); + } + + public function testItWorksWithNoneOperator(): void { + $customerId1 = $this->tester->createCustomer('c1@e.com'); + $this->createOrder($customerId1, Carbon::now(), 'paypal'); + $this->createOrder($customerId1, Carbon::now(), 'paypal'); + + $customerId2 = $this->tester->createCustomer('c2@e.com'); + $this->createOrder($customerId2, Carbon::now(), 'cheque'); + + $customerId3 = $this->tester->createCustomer('c3@e.com'); + $this->createOrder($customerId3, Carbon::now(), 'cheque'); + $this->createOrder($customerId3, Carbon::now(), 'paypal'); + + (new Subscriber)->withEmail('sub@e.com')->create(); + + $this->assertFilterReturnsEmails('none', ['paypal'], 1, ['sub@e.com', 'c2@e.com']); + $this->assertFilterReturnsEmails('none', ['cheque'], 1, ['sub@e.com', 'c1@e.com']); + $this->assertFilterReturnsEmails('none', ['doge'], 1000, ['sub@e.com', 'c1@e.com', 'c2@e.com', 'c3@e.com']); + $this->assertFilterReturnsEmails('none', ['paypal', 'cheque'], 1, ['sub@e.com']); + } + + public function testItWorksWithDateRanges(): void { + $customerId1 = $this->tester->createCustomer('c1@e.com'); + $this->createOrder($customerId1, Carbon::now()->subDays(2)->addMinute(), 'paypal'); + $this->createOrder($customerId1, Carbon::now()->subDays(5)->addMinute(), 'cheque'); + + $customerId2 = $this->tester->createCustomer('c2@e.com'); + $this->createOrder($customerId2, Carbon::now()->subDays(100)->addMinute(), 'cash'); + $this->assertFilterReturnsEmails('any', ['paypal'], 1, []); + $this->assertFilterReturnsEmails('any', ['paypal'], 2, ['c1@e.com']); + $this->assertFilterReturnsEmails('any', ['cheque'], 4, []); + $this->assertFilterReturnsEmails('any', ['cheque'], 5, ['c1@e.com']); + $this->assertFilterReturnsEmails('any', ['cash'], 99, []); + $this->assertFilterReturnsEmails('any', ['cash'], 100, ['c2@e.com']); + $this->assertFilterReturnsEmails('any', ['cash', 'paypal'], 100, ['c1@e.com', 'c2@e.com']); + + $this->assertFilterReturnsEmails('all', ['paypal'], 1, []); + $this->assertFilterReturnsEmails('all', ['paypal'], 2, ['c1@e.com']); + $this->assertFilterReturnsEmails('all', ['paypal', 'cheque'], 2, []); + $this->assertFilterReturnsEmails('all', ['paypal', 'cheque'], 5, ['c1@e.com']); + + $this->assertFilterReturnsEmails('none', ['paypal'], 1, ['c1@e.com', 'c2@e.com']); + $this->assertFilterReturnsEmails('none', ['paypal'], 2, ['c2@e.com']); + $this->assertFilterReturnsEmails('none', ['cheque'], 2, ['c1@e.com', 'c2@e.com']); + $this->assertFilterReturnsEmails('none', ['cheque'], 5, ['c2@e.com']); + } + + private function assertFilterReturnsEmails(string $operator, array $paymentMethods, int $days, array $expectedEmails): void { + $filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommerceUsedPaymentMethod::ACTION, [ + 'operator' => $operator, + 'payment_methods' => $paymentMethods, + 'used_payment_method_days' => $days, + ]); + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($filterData, $this->filter); + $this->assertEqualsCanonicalizing($expectedEmails, $emails); + } + + private function createOrder(int $customerId, Carbon $createdAt, string $paymentMethod): int { + $order = $this->tester->createWooCommerceOrder(); + $order->set_customer_id($customerId); + $order->set_date_created($createdAt->toDateTimeString()); + $order->set_status('wc-completed'); + $order->set_payment_method($paymentMethod); + $order->save(); + $this->tester->updateWooOrderStats($order->get_id()); + + return $order->get_id(); + } +} diff --git a/mailpoet/views/segments.html b/mailpoet/views/segments.html index a656d42ce5..68de02f98e 100644 --- a/mailpoet/views/segments.html +++ b/mailpoet/views/segments.html @@ -27,6 +27,7 @@ var mailpoet_can_use_woocommerce_subscriptions = <%= json_encode(can_use_woocommerce_subscriptions) %>; var mailpoet_woocommerce_currency_symbol = <%= json_encode(woocommerce_currency_symbol) %>; var mailpoet_woocommerce_countries = <%= json_encode(woocommerce_countries) %>; + var mailpoet_woocommerce_payment_methods = <%= json_encode(woocommerce_payment_methods) %>; var mailpoet_signup_forms = <%= json_encode(signup_forms) %>; <% endblock %> @@ -195,6 +196,7 @@ 'selectWooMembership': __('Search membership plans'), 'segmentsActiveSubscription': __('has active subscription'), 'woocommerceSubscriptions': _x('WooCommerce Subscriptions', 'Dynamic segment creation: User selects this to use any WooCommerce Subscriptions filters'), + 'wooUsedPaymentMethod': __('used payment method'), 'selectWooSubscription': __('Search subscriptions'), 'searchLists': __('Search lists'), 'subscriberTag': _x('tag', 'Subscriber tag'), @@ -233,6 +235,7 @@ 'selectWooPurchasedCategory': __('Search categories'), 'selectWooPurchasedProduct': __('Search products'), 'selectWooCountry': __('Search countries'), + 'selectWooPaymentMethods': __('Search payment methods'), 'woocommerce': _x('WooCommerce', 'Dynamic segment creation: User selects this to use any woocommerce filters'), 'dynamicSegmentSizeIsCalculated': __('Calculating segment size…'),