diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index 3dd872a333..61f8d6d8fe 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -454,6 +454,7 @@ class ContainerConfigurator implements IContainerConfigurator { $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\WooCommerceNumberOfReviews::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue::class)->setPublic(true); diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php index e74e4fe8c2..ee4ad92615 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php @@ -9,6 +9,7 @@ use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper; use MailPoet\Segments\DynamicSegments\Filters\EmailAction; use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny; use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction; +use MailPoet\Segments\DynamicSegments\Filters\FilterHelper; use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields; use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField; use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore; @@ -22,6 +23,7 @@ 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\WooCommerceNumberOfReviews; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; @@ -38,12 +40,22 @@ class FilterDataMapper { /** @var DateFilterHelper */ private $dateFilterHelper; + /** @var WooCommerceNumberOfReviews */ + private $wooCommerceNumberOfReviews; + + /** @var FilterHelper */ + private $filterHelper; + public function __construct( WPFunctions $wp, - DateFilterHelper $dateFilterHelper + DateFilterHelper $dateFilterHelper, + FilterHelper $filterHelper, + WooCommerceNumberOfReviews $wooCommerceNumberOfReviews ) { $this->wp = $wp; $this->dateFilterHelper = $dateFilterHelper; + $this->filterHelper = $filterHelper; + $this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews; } /** @@ -312,7 +324,7 @@ class FilterDataMapper { if (!isset($data['opens'])) { throw new InvalidFilterException('Missing number of opens', InvalidFilterException::MISSING_VALUE); } - $this->validateDaysPeriodData($data); + $this->filterHelper->validateDaysPeriodData($data); $filterData = [ 'opens' => $data['opens'], 'days' => $data['days'] ?? 0, @@ -362,7 +374,7 @@ class FilterDataMapper { $filterData['country_code'] = $data['country_code']; $filterData['operator'] = $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY; } elseif ($data['action'] === WooCommerceNumberOfOrders::ACTION_NUMBER_OF_ORDERS) { - $this->validateDaysPeriodData($data); + $this->filterHelper->validateDaysPeriodData($data); if ( !isset($data['number_of_orders_type']) || !isset($data['number_of_orders_count']) || $data['number_of_orders_count'] < 0 @@ -373,8 +385,15 @@ class FilterDataMapper { $filterData['number_of_orders_count'] = $data['number_of_orders_count']; $filterData['days'] = $data['days'] ?? 0; $filterData['timeframe'] = $data['timeframe']; + } elseif ($data['action'] === WooCommerceNumberOfReviews::ACTION) { + $this->wooCommerceNumberOfReviews->validateFilterData($data); + $filterData['days'] = $data['days']; + $filterData['count_type'] = $data['count_type']; + $filterData['count'] = $data['count']; + $filterData['rating'] = $data['rating']; + $filterData['timeframe'] = $data['timeframe']; } elseif ($data['action'] === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) { - $this->validateDaysPeriodData($data); + $this->filterHelper->validateDaysPeriodData($data); if ( !isset($data['total_spent_type']) || !isset($data['total_spent_amount']) || $data['total_spent_amount'] < 0 @@ -386,7 +405,7 @@ class FilterDataMapper { $filterData['days'] = $data['days'] ?? 0; $filterData['timeframe'] = $data['timeframe']; } elseif ($data['action'] === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) { - $this->validateDaysPeriodData($data); + $this->filterHelper->validateDaysPeriodData($data); if ( !isset($data['single_order_value_type']) || !isset($data['single_order_value_amount']) || $data['single_order_value_amount'] < 0 @@ -401,7 +420,7 @@ class FilterDataMapper { $filterData['operator'] = $data['operator']; $filterData['value'] = $data['value']; } elseif ($data['action'] === WooCommerceAverageSpent::ACTION) { - $this->validateDaysPeriodData($data); + $this->filterHelper->validateDaysPeriodData($data); if ( !isset($data['average_spent_type']) || !isset($data['average_spent_amount']) || $data['average_spent_amount'] < 0 @@ -510,20 +529,4 @@ class FilterDataMapper { } return new DynamicSegmentFilterData($filterType, $action, $filterData); } - - private function validateDaysPeriodData(array $data): void { - if (!isset($data['timeframe']) || !in_array($data['timeframe'], [DynamicSegmentFilterData::TIMEFRAME_ALL_TIME, DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST], true)) { - throw new InvalidFilterException('Missing timeframe type', InvalidFilterException::MISSING_VALUE); - } - - if ($data['timeframe'] === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) { - return; - } - - $days = intval($data['days'] ?? null); - - if ($days < 1) { - throw new InvalidFilterException('Missing number of days', InvalidFilterException::MISSING_VALUE); - } - } } diff --git a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php index 96c246b5a5..5690c6b217 100644 --- a/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php +++ b/mailpoet/lib/Segments/DynamicSegments/FilterFactory.php @@ -24,6 +24,7 @@ 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\WooCommerceNumberOfReviews; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; @@ -108,6 +109,9 @@ class FilterFactory { /** @var AutomationsEvents */ private $automationsEvents; + /** @var WooCommerceNumberOfReviews */ + private $wooCommerceNumberOfReviews; + public function __construct( EmailAction $emailAction, EmailActionClickAny $emailActionClickAny, @@ -119,6 +123,7 @@ class FilterFactory { WooCommerceCustomerTextField $wooCommerceCustomerTextField, EmailOpensAbsoluteCountAction $emailOpensAbsoluteCount, WooCommerceNumberOfOrders $wooCommerceNumberOfOrders, + WooCommerceNumberOfReviews $wooCommerceNumberOfReviews, WooCommerceTotalSpent $wooCommerceTotalSpent, WooCommerceMembership $wooCommerceMembership, WooCommercePurchaseDate $wooCommercePurchaseDate, @@ -141,6 +146,7 @@ class FilterFactory { $this->wooCommerceCategory = $wooCommerceCategory; $this->wooCommerceCountry = $wooCommerceCountry; $this->wooCommerceNumberOfOrders = $wooCommerceNumberOfOrders; + $this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews; $this->wooCommerceMembership = $wooCommerceMembership; $this->wooCommercePurchaseDate = $wooCommercePurchaseDate; $this->wooCommerceSubscription = $wooCommerceSubscription; @@ -253,6 +259,8 @@ class FilterFactory { return $this->wooCommerceUsedPaymentMethod; } elseif ($action === WooCommerceUsedShippingMethod::ACTION) { return $this->wooCommerceUsedShippingMethod; + } elseif ($action === WooCommerceNumberOfReviews::ACTION) { + return $this->wooCommerceNumberOfReviews; } elseif (in_array($action, WooCommerceCustomerTextField::ACTIONS)) { return $this->wooCommerceCustomerTextField; } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php b/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php index 1f4258bfed..cda3ef8ab0 100644 --- a/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/FilterHelper.php @@ -2,13 +2,15 @@ namespace MailPoet\Segments\DynamicSegments\Filters; +use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\SubscriberEntity; +use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException; use MailPoet\Util\Security; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; use MailPoetVendor\Doctrine\ORM\EntityManager; class FilterHelper { - /** @var EntityManager */ + /** @var EntityManager */ private $entityManager; public function __construct( @@ -60,4 +62,20 @@ class FilterHelper { $suffix = Security::generateRandomString(); return sprintf("%s_%s", $parameter, $suffix); } + + public function validateDaysPeriodData(array $data): void { + if (!isset($data['timeframe']) || !in_array($data['timeframe'], [DynamicSegmentFilterData::TIMEFRAME_ALL_TIME, DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST], true)) { + throw new InvalidFilterException('Missing timeframe type', InvalidFilterException::MISSING_VALUE); + } + + if ($data['timeframe'] === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) { + return; + } + + $days = intval($data['days'] ?? null); + + if ($days < 1) { + throw new InvalidFilterException('Missing number of days', InvalidFilterException::MISSING_VALUE); + } + } } diff --git a/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviews.php b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviews.php new file mode 100644 index 0000000000..67111fc178 --- /dev/null +++ b/mailpoet/lib/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviews.php @@ -0,0 +1,129 @@ +collationChecker = $collationChecker; + $this->filterHelper = $filterHelper; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + $commentsTable = $this->filterHelper->getPrefixedTable('comments'); + $commentMetaTable = $this->filterHelper->getPrefixedTable('commentmeta'); + $filterData = $filter->getFilterData(); + $this->validateFilterData((array)$filterData->getData()); + $type = strval($filterData->getParam('count_type')); + $rating = strval($filterData->getParam('rating')); + $days = intval($filterData->getParam('days')); + $count = intval($filterData->getParam('count')); + + $subscribersTable = $this->filterHelper->getSubscribersTable(); + $collation = $this->collationChecker->getCollateIfNeeded( + $subscribersTable, + 'email', + $commentsTable, + 'comment_author_email' + ); + + $isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME; + $joinCondition = "$subscribersTable.email = comments.comment_author_email $collation + AND comments.comment_type = 'review'"; + + if (!$isAllTime) { + $date = Carbon::now()->subDays($days); + $dateParam = $this->filterHelper->getUniqueParameterName('date'); + $joinCondition .= " AND comments.comment_date >= :$dateParam"; + $queryBuilder->setParameter($dateParam, $date->toDateTimeString()); + } + + $commentMetaJoinCondition = "comments.comment_ID = commentmeta.comment_id AND commentmeta.meta_key = 'rating'"; + + if ($rating !== 'any') { + $ratingParam = $this->filterHelper->getUniqueParameterName('rating'); + $commentMetaJoinCondition .= "AND commentmeta.meta_value = :$ratingParam"; + $queryBuilder->setParameter($ratingParam, $rating); + } + + $queryBuilder + ->leftJoin( + $subscribersTable, + $commentsTable, + 'comments', + $joinCondition + )->leftJoin( + 'comments', + $commentMetaTable, + 'commentmeta', + $commentMetaJoinCondition + ); + + $queryBuilder->groupBy('inner_subscriber_id'); + + $countParam = $this->filterHelper->getUniqueParameterName('count'); + + switch ($type) { + case '=': + $queryBuilder->having("COUNT(commentmeta.meta_value) = :$countParam"); + break; + case '!=': + $queryBuilder->having("COUNT(commentmeta.meta_value) != :$countParam"); + break; + case '>': + $queryBuilder->having("COUNT(commentmeta.meta_value) > :$countParam"); + break; + case '<': + $queryBuilder->having("COUNT(commentmeta.meta_value) < :$countParam"); + break; + } + + $queryBuilder->setParameter($countParam, $count, 'integer'); + return $queryBuilder; + } + + public function validateFilterData(array $data): void { + if (!isset($data['rating'])) { + throw new InvalidFilterException('Missing rating', InvalidFilterException::MISSING_VALUE); + } + $validRatings = ['1', '2', '3', '4', '5', 'any']; + if (!in_array($data['rating'], $validRatings, true)) { + throw new InvalidFilterException('Invalid rating', InvalidFilterException::MISSING_VALUE); + } + if (!isset($data['count_type'])) { + throw new InvalidFilterException('Missing count type', InvalidFilterException::MISSING_VALUE); + } + $type = $data['count_type']; + $validTypes = [ + '=', + '!=', + '>', + '<', + ]; + if (!in_array($type, $validTypes, true)) { + throw new InvalidFilterException('Invalid count type', InvalidFilterException::INVALID_TYPE); + } + + if (!isset($data['count'])) { + throw new InvalidFilterException('Missing review count', InvalidFilterException::MISSING_VALUE); + } + $this->filterHelper->validateDaysPeriodData($data); + } +} diff --git a/mailpoet/tests/_support/IntegrationTester.php b/mailpoet/tests/_support/IntegrationTester.php index 2528638c73..cfb725bc18 100644 --- a/mailpoet/tests/_support/IntegrationTester.php +++ b/mailpoet/tests/_support/IntegrationTester.php @@ -18,6 +18,7 @@ use MailPoet\InvalidStateException; use MailPoet\Segments\DynamicSegments\Filters\Filter; use MailPoet\Util\Security; use MailPoet\WooCommerce\Helper; +use MailPoetVendor\Carbon\Carbon; use MailPoetVendor\Doctrine\DBAL\Driver\Statement; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; use MailPoetVendor\Doctrine\ORM\EntityManager; @@ -57,6 +58,8 @@ class IntegrationTester extends \Codeception\Actor { private $createdUsers = []; + private $createdCommentIds = []; + public function __construct( Scenario $scenario ) { @@ -177,6 +180,28 @@ class IntegrationTester extends \Codeception\Actor { return $order; } + public function createWooProductReview(int $customerId, string $customerEmail, int $productId, int $rating, Carbon $date = null): int { + if ($date === null) { + $date = Carbon::now()->subDay(); + } + $commentId = wp_insert_comment([ + 'comment_type' => 'review', + 'user_id' => $customerId, + 'comment_author_email' => $customerEmail, + 'comment_post_ID' => $productId, + 'comment_parent' => 0, + 'comment_date' => $date->toDateTimeString(), + 'comment_approved' => 1, + 'comment_content' => "This is a $rating star review", + ]); + if (!is_int($commentId)) { + throw new \Exception('Failed to insert review comment'); + } + add_comment_meta($commentId, 'rating', $rating, true); + $this->createdCommentIds[] = $commentId; + return $commentId; + } + public function createWooCommerceCoupon(array $data): void { $coupon = new WC_Coupon(); @@ -348,8 +373,16 @@ class IntegrationTester extends \Codeception\Actor { public function cleanup() { $this->deleteWordPressTerms(); $this->deleteCreatedUsers(); + $this->deleteCreatedComments(); $this->deleteTestWooProducts(); $this->deleteTestWooOrders(); $this->deleteTestWooCoupons(); } + + private function deleteCreatedComments() { + foreach ($this->createdCommentIds as $commentId) { + wp_delete_comment($commentId, true); + } + $this->createdCommentIds = []; + } } diff --git a/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviewsTest.php b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviewsTest.php new file mode 100644 index 0000000000..946ac8d3b1 --- /dev/null +++ b/mailpoet/tests/integration/Segments/DynamicSegments/Filters/WooCommerceNumberOfReviewsTest.php @@ -0,0 +1,239 @@ +filter = $this->diContainer->get(WooCommerceNumberOfReviews::class); + $this->productId = $this->tester->createWooCommerceProduct(['name' => 'Some really fantastic product'])->get_id(); + } + + public function testAnyRatingOnlyReturnsSubscribersWithRating(): void { + $customerId = $this->tester->createCustomer('1@e.com'); + $this->tester->createCustomer('noreview@e.com'); + $this->tester->createWooProductReview($customerId, '1@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->assertFilterReturnsEmails('any', '>', 0, 200, 'inTheLast', ['1@e.com']); + } + + public function testItWorksWithOverAllTimeOption(): void { + $customerId = $this->tester->createCustomer('1@e.com'); + $this->tester->createWooProductReview($customerId, '1@e.com', $this->productId, 1, Carbon::now()->subDays(20)); + $this->assertFilterReturnsEmails('any', '>', 0, 2, 'inTheLast', []); + $this->assertFilterReturnsEmails('any', '>', 0, 2, 'allTime', ['1@e.com']); + } + + public function testItHandlesExactNumberOfReviews(): void { + $customerId = $this->tester->createCustomer('test1@e.com'); + $this->tester->createWooProductReview($customerId, 'test1@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->tester->createWooProductReview($customerId, 'test1@e.com', $this->productId, 2, Carbon::now()->subDay()); + $this->assertFilterReturnsEmails('any', '=', 2, 200, 'inTheLast', ['test1@e.com']); + } + + public function testItHandlesGreaterThan(): void { + $customerId = $this->tester->createCustomer('greaterThanTest@e.com'); + $this->tester->createWooProductReview($customerId, 'greaterthantest@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->tester->createWooProductReview($customerId, 'greaterthantest@e.com', $this->productId, 2, Carbon::now()->subDay()); + $customerId = $this->tester->createCustomer('oneReviewTest@e.com'); + $this->tester->createWooProductReview($customerId, 'onereviewtest@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->assertFilterReturnsEmails('any', '>', 1, 200, 'inTheLast', ['greaterthantest@e.com']); + } + + public function testItHandlesLessThan(): void { + $this->tester->createCustomer('lessthantest@e.com'); + $customerId = $this->tester->createCustomer('onereviewtest@e.com'); + $this->tester->createWooProductReview($customerId, 'onereviewtest@e.com', $this->productId, 1, Carbon::now()->subDay()); + $customerId2 = $this->tester->createCustomer('tworeviews@e.com'); + $this->tester->createWooProductReview($customerId, 'tworeviews@e.com', $this->productId, 2, Carbon::now()->subDay()); + $this->tester->createWooProductReview($customerId, 'tworeviews@e.com', $this->productId, 2, Carbon::now()->subDay()); + $this->assertFilterReturnsEmails('any', '<', 1, 200, 'inTheLast', ['lessthantest@e.com']); + $this->assertFilterReturnsEmails('any', '<', 2, 200, 'inTheLast', ['lessthantest@e.com', 'onereviewtest@e.com']); + } + + public function testItHandlesNotEquals(): void { + $this->tester->createCustomer('notequalstest@e.com'); + $customerId = $this->tester->createCustomer('onereviewtest@e.com'); + $this->tester->createWooProductReview($customerId, 'onereviewtest@e.com', $this->productId, 1, Carbon::now()->subDay()); + $customerId = $this->tester->createCustomer('tworeviewstest@e.com'); + $this->tester->createWooProductReview($customerId, 'tworeviewstest@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->tester->createWooProductReview($customerId, 'tworeviewstest@e.com', $this->productId, 2, Carbon::now()->subDay()); + + $this->assertFilterReturnsEmails('any', '!=', 1, 200, 'inTheLast', ['notequalstest@e.com', 'tworeviewstest@e.com']); + } + + public function testItHandlesCustomerWithNoReviews(): void { + $this->tester->createCustomer('test2@e.com'); + $this->assertFilterReturnsEmails('any', '>', 0, 200, 'inTheLast', []); + } + + public function testItIncludesCustomersWithNoReviewsWhenUsingLessThan(): void { + $this->tester->createCustomer('test1@e.com'); + $customerId = $this->tester->createCustomer('test2@e.com'); + $this->tester->createWooProductReview($customerId, 'test2@e.com', $this->productId, 1, Carbon::now()->subDay()); + $this->assertFilterReturnsEmails('any', '<', 1, 200, 'inTheLast', ['test1@e.com']); + } + + public function testFiltersByDifferentRatings(): void { + $customerOneId = $this->tester->createCustomer('customer-one@test.com'); + $this->tester->createWooProductReview($customerOneId, 'customer-one@test.com', $this->productId, 5); + $this->tester->createWooProductReview($customerOneId, 'customer-one@test.com', $this->productId, 5); + $this->tester->createWooProductReview($customerOneId, 'customer-one@test.com', $this->productId, 3); + + $customerTwoId = $this->tester->createCustomer('customer-two@test.com'); + $this->tester->createWooProductReview($customerTwoId, 'customer-two@test.com', $this->productId, 4); + $this->tester->createWooProductReview($customerTwoId, 'customer-two@test.com', $this->productId, 4); + $this->tester->createWooProductReview($customerTwoId, 'customer-two@test.com', $this->productId, 4); + + $customerThreeId = $this->tester->createCustomer('customer-three@test.com'); + $this->tester->createWooProductReview($customerThreeId, 'customer-three@test.com', $this->productId, 2); + $this->tester->createWooProductReview($customerThreeId, 'customer-three@test.com', $this->productId, 5); + + $this->assertFilterReturnsEmails('5', '>', 1, 200, 'inTheLast', ['customer-one@test.com']); + $this->assertFilterReturnsEmails('4', '=', 3, 200, 'inTheLast', ['customer-two@test.com']); + $this->assertFilterReturnsEmails('2', '=', 1, 200, 'inTheLast', ['customer-three@test.com']); + } + + public function testFiltersByDifferentDates(): void { + $customerFourId = $this->tester->createCustomer('1@e.com'); + $this->tester->createWooProductReview($customerFourId, '1@e.com', $this->productId, 5, Carbon::now()->subDays(6)); + + $customerFiveId = $this->tester->createCustomer('2@e.com'); + $this->tester->createWooProductReview($customerFiveId, '2@e.com', $this->productId, 5, Carbon::now()->subWeeks(3)); + + $customerSixId = $this->tester->createCustomer('3@e.com'); + $this->tester->createWooProductReview($customerSixId, '3@e.com', $this->productId, 4, Carbon::now()->subWeeks(6)); + + $this->assertFilterReturnsEmails('5', '=', 1, 7, 'inTheLast', ['1@e.com']); + $this->assertFilterReturnsEmails('5', '=', 1, 30, 'inTheLast', ['1@e.com', '2@e.com']); + $this->assertFilterReturnsEmails('4', '=', 1, 50, 'inTheLast', ['3@e.com']); + } + + public function testItValidatesRatingPresence(): void { + $this->expectException(InvalidFilterException::class); + $this->expectExceptionCode(InvalidFilterException::MISSING_VALUE); + $this->expectExceptionMessage('Missing rating'); + $this->filter->validateFilterData([ + 'action' => 'numberOfReviews', + 'count_type' => '!=', + 'days' => '10', + 'count' => '3', + 'timeframe' => 'inTheLast', + ]); + } + + /** + * @dataProvider ratingDataProvider + */ + public function testItValidatesRatingValue(string $rating, bool $shouldFail): void { + $data = [ + 'action' => 'numberOfReviews', + 'rating' => $rating, + 'days' => '10', + 'count' => '2', + 'count_type' => '!=', + 'timeframe' => 'inTheLast', + ]; + + if ($shouldFail) { + $this->expectException(InvalidFilterException::class); + $this->expectExceptionMessage('Invalid rating'); + $this->expectExceptionCode(InvalidFilterException::MISSING_VALUE); + } + + $this->filter->validateFilterData($data); + } + + public function ratingDataProvider(): array { + return [ + 'Invalid rating value' => ['6', true], + 'Valid rating value 1' => ['1', false], + 'Valid rating value 2' => ['2', false], + 'Valid rating value 3' => ['3', false], + 'Valid rating value 4' => ['4', false], + 'Valid rating value 5' => ['5', false], + 'Valid rating value any' => ['any', false], + ]; + } + + public function testItValidatesCountType(): void { + $this->expectException(InvalidFilterException::class); + $this->expectExceptionCode(InvalidFilterException::MISSING_VALUE); + $this->expectExceptionMessage('Missing count type'); + $this->filter->validateFilterData([ + 'action' => 'numberOfReviews', + 'rating' => '3', + 'days' => '10', + 'count' => '3', + 'timeframe' => 'inTheLast', + ]); + } + + public function testItValidatesCountTypeOptions(): void { + $this->expectException(InvalidFilterException::class); + $this->expectExceptionCode(InvalidFilterException::INVALID_TYPE); + $this->expectExceptionMessage('Invalid count type'); + $this->filter->validateFilterData([ + 'action' => 'numberOfReviews', + 'rating' => '3', + 'days' => '10', + 'count' => '3', + 'count_type' => 'invalid', + 'timeframe' => 'inTheLast', + ]); + } + + public function testItValidatesCount(): void { + $this->expectException(InvalidFilterException::class); + $this->expectExceptionCode(InvalidFilterException::MISSING_VALUE); + $this->expectExceptionMessage('Missing review count'); + $this->filter->validateFilterData([ + 'action' => 'numberOfReviews', + 'count_type' => '!=', + 'rating' => '3', + 'days' => '10', + 'timeframe' => 'inTheLast', + ]); + } + + public function _after(): void { + parent::_after(); + $this->cleanUp(); + } + + private function assertFilterReturnsEmails(string $rating, string $countType, int $count, int $days, string $timeframe, array $expectedEmails): void { + $data = new DynamicSegmentFilterData( + DynamicSegmentFilterData::TYPE_WOOCOMMERCE, + WooCommerceNumberOfReviews::ACTION, + [ + 'days' => $days, + 'count_type' => $countType, + 'count' => $count, + 'timeframe' => $timeframe, + 'rating' => $rating, + ] + ); + $emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($data, $this->filter); + $this->assertEqualsCanonicalizing($expectedEmails, $emails); + } + + private function cleanUp(): void { + global $wpdb; + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}wc_customer_lookup"); + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}wc_order_stats"); + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}comments"); + $this->connection->executeQuery("TRUNCATE TABLE {$wpdb->prefix}commentmeta"); + } +} diff --git a/mailpoet/tests/unit/Segments/DynamicSegments/FilterDataMapperTest.php b/mailpoet/tests/unit/Segments/DynamicSegments/FilterDataMapperTest.php index da8bd75601..51e8d0b70b 100644 --- a/mailpoet/tests/unit/Segments/DynamicSegments/FilterDataMapperTest.php +++ b/mailpoet/tests/unit/Segments/DynamicSegments/FilterDataMapperTest.php @@ -9,6 +9,7 @@ use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper; use MailPoet\Segments\DynamicSegments\Filters\EmailAction; use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny; use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction; +use MailPoet\Segments\DynamicSegments\Filters\FilterHelper; use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields; use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField; use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore; @@ -19,6 +20,7 @@ use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders; +use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription; @@ -33,12 +35,12 @@ class FilterDataMapperTest extends \MailPoetUnitTest { $wp = $this->makeEmpty(WPFunctions::class, [ 'hasFilter' => false, ]); - - $filterHelper = $this->getMockBuilder(DateFilterHelper::class) + $wcNumberOfReviews = $this->makeEmpty(WooCommerceNumberOfReviews::class); + $dateFilterHelper = $this->getMockBuilder(DateFilterHelper::class) ->setMethodsExcept(['getAbsoluteDateOperators', 'getRelativeDateOperators', 'getValidOperators']) ->getMock(); - - $this->mapper = new FilterDataMapper($wp, $filterHelper); + $filterHelper = $this->makeEmptyExcept(FilterHelper::class, 'validateDaysPeriodData'); + $this->mapper = new FilterDataMapper($wp, $dateFilterHelper, $filterHelper, $wcNumberOfReviews); } public function testItChecksFiltersArePresent(): void { @@ -885,6 +887,35 @@ class FilterDataMapperTest extends \MailPoetUnitTest { ]]]); } + public function testItMapsWooNumberOfReviews(): void { + $data = ['filters' => [[ + 'segmentType' => DynamicSegmentFilterData::TYPE_WOOCOMMERCE, + 'action' => 'numberOfReviews', + 'operator' => 'all', + 'count_type' => '!=', + 'rating' => '3', + 'days' => '10', + 'count' => '3', + 'timeframe' => 'inTheLast', + ]]]; + $filters = $this->mapper->map($data); + expect($filters)->array(); + expect($filters)->count(1); + $filter = reset($filters); + $this->assertInstanceOf(DynamicSegmentFilterData::class, $filter); + expect($filter)->isInstanceOf(DynamicSegmentFilterData::class); + expect($filter->getFilterType())->equals(DynamicSegmentFilterData::TYPE_WOOCOMMERCE); + expect($filter->getAction())->equals('numberOfReviews'); + expect($filter->getData())->equals([ + 'connect' => 'and', + 'days' => '10', + 'count_type' => '!=', + 'count' => '3', + 'rating' => '3', + 'timeframe' => 'inTheLast', + ]); + } + /** * @dataProvider dateFieldActions */