diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index 9a9699cb96..de8e81ed7e 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -437,6 +437,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField::class)->setPublic(true); $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); diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php index 49738e0007..c134c408e9 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php @@ -18,6 +18,7 @@ use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; @@ -323,6 +324,15 @@ class FilterDataMapper { $filterData['operator'] = $data['operator']; $filterData['payment_methods'] = $data['payment_methods']; $filterData['used_payment_method_days'] = intval($data['used_payment_method_days']); + } elseif (in_array($data['action'], WooCommerceCustomerTextField::ACTIONS)) { + if (empty($data['value'])) throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE); + if (empty($data['operator'])) throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_VALUE); + if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) { + throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_VALUE); + } + $filterData['value'] = $data['value']; + $filterData['operator'] = $data['operator']; + $filterData['action'] = $data['action']; } 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 36bc6a9a41..68ad911c42 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php @@ -20,6 +20,7 @@ use MailPoet\Segments\DynamicSegments\Filters\UserRole; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; @@ -96,6 +97,9 @@ class FilterFactory { /** @var WooCommerceUsedPaymentMethod */ private $wooCommerceUsedPaymentMethod; + /** @var WooCommerceCustomerTextField */ + private $wooCommerceCustomerTextField; + public function __construct( EmailAction $emailAction, EmailActionClickAny $emailActionClickAny, @@ -104,6 +108,7 @@ class FilterFactory { WooCommerceProduct $wooCommerceProduct, WooCommerceCategory $wooCommerceCategory, WooCommerceCountry $wooCommerceCountry, + WooCommerceCustomerTextField $wooCommerceCustomerTextField, EmailOpensAbsoluteCountAction $emailOpensAbsoluteCount, WooCommerceNumberOfOrders $wooCommerceNumberOfOrders, WooCommerceTotalSpent $wooCommerceTotalSpent, @@ -142,6 +147,7 @@ class FilterFactory { $this->subscribedViaForm = $subscribedViaForm; $this->wooCommerceAverageSpent = $wooCommerceAverageSpent; $this->wooCommerceUsedPaymentMethod = $wooCommerceUsedPaymentMethod; + $this->wooCommerceCustomerTextField = $wooCommerceCustomerTextField; } public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter { @@ -231,6 +237,8 @@ class FilterFactory { return $this->wooCommerceAverageSpent; } elseif ($action === WooCommerceUsedPaymentMethod::ACTION) { return $this->wooCommerceUsedPaymentMethod; + } elseif (in_array($action, WooCommerceCustomerTextField::ACTIONS)) { + return $this->wooCommerceCustomerTextField; } return $this->wooCommerceCategory; } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceCustomerTextField.php b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceCustomerTextField.php new file mode 100644 index 0000000000..a84417dbb8 --- /dev/null +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceCustomerTextField.php @@ -0,0 +1,103 @@ +filterHelper = $filterHelper; + $this->wooFilterHelper = $wooFilterHelper; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + $filterData = $filter->getFilterData(); + $action = $filterData->getParam('action'); + $value = $filterData->getParam('value'); + $operator = $filterData->getParam('operator'); + + if (!is_string($action)) { + throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_VALUE); + } + + if (!is_string($value)) { + throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE); + } + + if (!is_string($operator)) { + throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_VALUE); + } + + $customerLookupAlias = $this->wooFilterHelper->applyCustomerLookupJoin($queryBuilder); + $column = sprintf("%s.%s", $customerLookupAlias, $this->getColumnNameForAction($action)); + $parameter = $this->filterHelper->getUniqueParameterName('customerTextField'); + + switch ($operator) { + case DynamicSegmentFilterData::OPERATOR_IS: + $queryBuilder->andWhere("$column = :$parameter"); + break; + case DynamicSegmentFilterData::OPERATOR_IS_NOT: + $queryBuilder->andWhere("$column != :$parameter"); + break; + case DynamicSegmentFilterData::OPERATOR_CONTAINS: + $queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter")); + $value = '%' . Helpers::escapeSearch($value) . '%'; + break; + case DynamicSegmentFilterData::OPERATOR_NOT_CONTAINS: + $queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter")); + $value = '%' . Helpers::escapeSearch($value) . '%'; + break; + case DynamicSegmentFilterData::OPERATOR_STARTS_WITH: + $queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter")); + $value = Helpers::escapeSearch($value) . '%'; + break; + case DynamicSegmentFilterData::OPERATOR_NOT_STARTS_WITH: + $queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter")); + $value = Helpers::escapeSearch($value) . '%'; + break; + case DynamicSegmentFilterData::OPERATOR_ENDS_WITH: + $queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter")); + $value = '%' . Helpers::escapeSearch($value); + break; + case DynamicSegmentFilterData::OPERATOR_NOT_ENDS_WITH: + $queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter")); + $value = '%' . Helpers::escapeSearch($value); + break; + default: + throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR); + } + + $queryBuilder->setParameter($parameter, $value); + + return $queryBuilder; + } + + private function getColumnNameForAction(string $field): string { + switch ($field) { + case self::CITY: + return 'city'; + case self::POSTAL_CODE: + return 'postcode'; + } + + throw new InvalidFilterException('Invalid action'); + } +} diff --git a/mailpoet/tests/_support/IntegrationTester.php b/mailpoet/tests/_support/IntegrationTester.php index 662f2a2ba2..2528638c73 100644 --- a/mailpoet/tests/_support/IntegrationTester.php +++ b/mailpoet/tests/_support/IntegrationTester.php @@ -156,6 +156,14 @@ class IntegrationTester extends \Codeception\Actor { $order->set_date_created($data['date_created']); } + if (isset($data['billing_postcode'])) { + $order->set_billing_postcode($data['billing_postcode']); + } + + if (isset($data['billing_city'])) { + $order->set_billing_city($data['billing_city']); + } + if (isset($data['total'])) { $order->set_total($data['total']); } diff --git a/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCustomerTextFieldTest.php b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCustomerTextFieldTest.php new file mode 100644 index 0000000000..004c88d429 --- /dev/null +++ b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCustomerTextFieldTest.php @@ -0,0 +1,208 @@ +filter = $this->diContainer->get(WooCommerceCustomerTextField::class); + } + + public function testEquals(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'is', 'Minneapolis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'is', 'Anchorage', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'is', 'eapolis', []); + $this->assertFilterReturnsEmails('customerInCity', 'is', 'Anchorag', []); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'is', '55111', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'is', '99540', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'is', '9954', []); + $this->assertFilterReturnsEmails('customerInPostalCode', 'is', '9540', []); + } + + public function testNotEquals(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'isNot', 'Minneapolis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'isNot', 'Anchorage', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'isNot', 'eapolis', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'isNot', 'Anchorag', ['1@e.com', '2@e.com']); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'isNot', '55111', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'isNot', '99540', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'isNot', '9540', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'isNot', '9954', ['1@e.com', '2@e.com']); + } + + public function testStartsWith() { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'Minneapolis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'Minn', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'Anchorage', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'A', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'Anchoragee', []); + $this->assertFilterReturnsEmails('customerInCity', 'startsWith', 'inneapolis', []); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'startsWith', '55111', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'startsWith', '5', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'startsWith', '99540', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'startsWith', '9954', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'startsWith', '6', []); + } + + public function testDoesNotStartWith(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'Minneapolis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'Minn', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'Anchorage', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'A', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'Anchoragee', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notStartsWith', 'inneapolis', ['1@e.com', '2@e.com']); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'notStartsWith', '55111', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notStartsWith', '5', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notStartsWith', '99540', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notStartsWith', '9954', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notStartsWith', '6', ['1@e.com', '2@e.com']); + } + + public function testEndsWith(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'Minneapolis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'lis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'Minn', []); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'Anchorage', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'age', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'A', []); + $this->assertFilterReturnsEmails('customerInCity', 'endsWith', 'liss', []); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'endsWith', '55111', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'endsWith', '1', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'endsWith', '99540', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'endsWith', '40', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'endsWith', '2', []); + } + + public function testDoesNotEndWith(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'notEndsWith', 'Minneapolis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notEndsWith', 'lis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notEndsWith', 'Anchorage', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notEndsWith', 'e', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notEndsWith', 'q', ['1@e.com', '2@e.com']); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'notEndsWith', '55111', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notEndsWith', '1', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notEndsWith', '99540', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notEndsWith', '40', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notEndsWith', '6', ['1@e.com', '2@e.com']); + } + + public function testContains(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'Minneapolis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'lis', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'Min', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'eapo', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'Anchorage', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'age', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'Anc', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'hora', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'q', []); + $this->assertFilterReturnsEmails('customerInCity', 'contains', 'e', ['1@e.com', '2@e.com']); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '55111', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '55', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '111', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '51', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '99540', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '99', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '40', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '954', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '6', []); + $this->assertFilterReturnsEmails('customerInPostalCode', 'contains', '5', ['1@e.com', '2@e.com']); + } + + public function testNotContains(): void { + $this->createCustomer('1@e.com', 'Minneapolis', '55111'); + $this->createCustomer('2@e.com', 'Anchorage', '99540'); + + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'Minneapolis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'lis', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'Min', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'eapo', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'Anchorage', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'age', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'Anc', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'hora', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'q', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('customerInCity', 'notContains', 'e', []); + + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '55111', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '55', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '111', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '51', ['2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '99540', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '99', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '40', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '954', ['1@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '6', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('customerInPostalCode', 'notContains', '5', []); + } + + public function testContainsDoesNotBreakIfItIncludesPercentSymbol(): void { + $this->createCustomer('1@e.com', 'Minn%eapolis', '55111'); + $this->assertFilterReturnsEmails('customerInCity', 'contains', '%', ['1@e.com']); + } + + private function assertFilterReturnsEmails(string $action, string $operator, string $value, array $expectedEmails): void { + $filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, $action, [ + 'operator' => $operator, + 'value' => $value, + 'action' => $action, + ]); + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($filterData, $this->filter); + $this->assertEqualsCanonicalizing($expectedEmails, $emails); + } + + private function createCustomer(string $email, string $city, string $postalCode): void { + $this->tester->createCustomer($email); + $order = $this->tester->createWooCommerceOrder([ + 'billing_email' => $email, + 'billing_postcode' => $postalCode, + 'billing_city' => $city, + 'status' => 'wc-complete', + ]); + $order->save(); + } + + public function _after() { + parent::_after(); + global $wpdb; + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}wc_customer_lookup"); + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}wc_order_stats"); + } +}