Update the queries of the 'used shipping method' filter

This commit changes the logic of the code that builds the queries for
the 'used shipping method' filter. The initial implementation relied
only on the shipping method name that is stored in
wp_woocommerce_order_items.  This implementation did not work as the
name is not unique. The new implementation use the shipping method id
and the instance id that are stored in wp_woocommerce_order_itemmeta and
the combination of both fields is unique.

[MAILPOET-4992]
This commit is contained in:
Rodrigo Primo
2023-06-13 12:22:17 -03:00
committed by Aschepikov
parent 993e292fd0
commit f20878f7f3
2 changed files with 120 additions and 66 deletions

View File

@ -41,14 +41,15 @@ class WooCommerceUsedShippingMethod implements Filter {
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
$filterData = $filter->getFilterData();
$operator = $filterData->getParam('operator');
$shippingMethods = $filterData->getParam('shipping_methods');
list($shippingMethodIds, $instanceIds) = $this->extractShippingMethodIdsAndInstanceIds((array)$filterData->getParam('shipping_methods'));
$days = $filterData->getParam('used_shipping_method_days');
if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
}
if (!is_array($shippingMethods) || count($shippingMethods) < 1) {
if (!is_array($shippingMethodIds) || empty($shippingMethodIds) || !is_array($instanceIds) || empty($instanceIds)) {
throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
}
@ -65,58 +66,106 @@ class WooCommerceUsedShippingMethod implements Filter {
switch ($operator) {
case DynamicSegmentFilterData::OPERATOR_ANY:
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $shippingMethods, $date);
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $shippingMethodIds, $instanceIds, $date);
break;
case DynamicSegmentFilterData::OPERATOR_ALL:
$this->applyForAllOperator($queryBuilder, $includedStatuses, $shippingMethods, $date);
$this->applyForAllOperator($queryBuilder, $includedStatuses, $shippingMethodIds, $instanceIds, $date);
break;
case DynamicSegmentFilterData::OPERATOR_NONE:
$this->applyForNoneOperator($queryBuilder, $includedStatuses, $shippingMethods, $date);
$this->applyForNoneOperator($queryBuilder, $includedStatuses, $shippingMethodIds, $instanceIds, $date);
break;
}
return $queryBuilder;
}
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethods, Carbon $date): void {
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodIds, array $instanceIds, Carbon $date): void {
$dateParam = $this->filterHelper->getUniqueParameterName('date');
$shippingMethodParam = $this->filterHelper->getUniqueParameterName('shippingMethod');
$shippingMethodsParam = $this->filterHelper->getUniqueParameterName('shippingMethods');
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
$orderItemsTableAlias = 'orderItems';
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
$orderItemMetaTableAlias1 = 'orderItemMeta1';
$orderItemMetaTableAlias2 = 'orderItemMeta2';
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
$queryBuilder
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias1, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias1.order_item_id")
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias2, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias2.order_item_id")
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
->andWhere("$orderItemsTableAlias.order_item_name IN (:$shippingMethodParam)")
->andWhere("$orderItemsTableAlias.order_item_type = 'shipping'")
->andWhere("$orderItemMetaTableAlias1.meta_key = 'method_id'")
->andWhere("$orderItemMetaTableAlias1.meta_value IN (:$shippingMethodsParam)")
->andWhere("$orderItemMetaTableAlias2.meta_key = 'instance_id'")
->andWhere("$orderItemMetaTableAlias2.meta_value IN (:$instanceIdsParam)")
->setParameter($dateParam, $date->toDateTimeString())
->setParameter($shippingMethodParam, $shippingMethods, Connection::PARAM_STR_ARRAY);
->setParameter($shippingMethodsParam, $shippingMethodIds, Connection::PARAM_STR_ARRAY)
->setParameter($instanceIdsParam, $instanceIds, Connection::PARAM_STR_ARRAY);
}
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethods, Carbon $date): void {
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodIds, array $instanceIds, Carbon $date): void {
$dateParam = $this->filterHelper->getUniqueParameterName('date');
$orderItemTypeParam = $this->filterHelper->getUniqueParameterName('orderItemType');
$shippingMethodsParam = $this->filterHelper->getUniqueParameterName('shippingMethod');
$shippingMethodsParam = $this->filterHelper->getUniqueParameterName('shippingMethods');
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
$orderItemsAlias = 'orderItems';
$orderItemsTableAlias = 'orderItems';
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
$orderItemMetaTableAlias1 = 'orderItemMeta1';
$orderItemMetaTableAlias2 = 'orderItemMeta2';
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
$queryBuilder
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsAlias, "$orderStatsAlias.order_id = $orderItemsAlias.order_id")
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias1, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias1.order_item_id")
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias2, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias2.order_item_id")
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
->andWhere("$orderItemsAlias.order_item_type = :$orderItemTypeParam")
->andWhere("$orderItemsAlias.order_item_name IN (:$shippingMethodsParam)")
->andWhere("$orderItemsTableAlias.order_item_type = :$orderItemTypeParam")
->andWhere("$orderItemMetaTableAlias1.meta_key = 'method_id'")
->andWhere("$orderItemMetaTableAlias1.meta_value IN (:$shippingMethodsParam)")
->andWhere("$orderItemMetaTableAlias2.meta_key = 'instance_id'")
->andWhere("$orderItemMetaTableAlias2.meta_value IN (:$instanceIdsParam)")
->setParameter($dateParam, $date->toDateTimeString())
->setParameter($orderItemTypeParam, 'shipping')
->setParameter($shippingMethodsParam, $shippingMethods, Connection::PARAM_STR_ARRAY)
->groupBy('inner_subscriber_id')->having("COUNT(DISTINCT $orderItemsAlias.order_item_name) = " . count($shippingMethods));
->setParameter($shippingMethodsParam, $shippingMethodIds, Connection::PARAM_STR_ARRAY)
->setParameter($instanceIdsParam, $instanceIds, Connection::PARAM_STR_ARRAY)
->groupBy('inner_subscriber_id')
->having("COUNT(DISTINCT(CONCAT($orderItemMetaTableAlias1.meta_value, $orderItemMetaTableAlias2.meta_value))) = " . count($shippingMethodIds));
}
private function applyForNoneOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethods, Carbon $date): void {
private function applyForNoneOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodIds, array $instanceIds, Carbon $date): void {
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
$this->applyForAnyOperator($subQuery, $includedStatuses, $shippingMethods, $date);
$this->applyForAnyOperator($subQuery, $includedStatuses, $shippingMethodIds, $instanceIds, $date);
$subscribersTable = $this->filterHelper->getSubscribersTable();
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
}
/**
* Extracts shipping method ids and instance ids from the given array of strings.
* The format of each shipping method string is "shippingMethod:instanceId". For example,
* "flat_rate:1" or "local_pickup:2".
*
* @param array $shippingMethodStrings
* @return array[]
*/
private function extractShippingMethodIdsAndInstanceIds(array $shippingMethodStrings): array {
$shippingMethodIds = [];
$instanceIds = [];
foreach ($shippingMethodStrings as $shippingMethodString) {
if (preg_match('/^\w+:\d+$/', $shippingMethodString)) {
$parts = preg_split('/:/', $shippingMethodString);
if (is_array($parts) && is_string($parts[0]) && is_string($parts[1])) {
$shippingMethodIds[] = $parts[0];
$instanceIds[] = $parts[1];
}
}
}
return [$shippingMethodIds, $instanceIds];
}
}

View File

@ -23,99 +23,104 @@ class WooCommerceUsedShippingMethodTest extends \MailPoetTest {
$customerId1 = $this->tester->createCustomer('c1@e.com');
$customerId2 = $this->tester->createCustomer('c2@e.com');
$customerId3 = $this->tester->createCustomer('c3@e.com');
$customerId4 = $this->tester->createCustomer('c4@e.com');
$this->createOrder($customerId1, Carbon::now(), 'Flat rate');
$this->createOrder($customerId2, Carbon::now(), 'Local pickup');
$this->createOrder($customerId3, Carbon::now(), 'Flat rate');
$this->createOrder($customerId3, Carbon::now(), 'Free shipping');
$this->createOrder($customerId1, Carbon::now(), 'flat_rate', 1);
$this->createOrder($customerId2, Carbon::now(), 'local_pickup', 2);
$this->createOrder($customerId2, Carbon::now(), 'flat_rate', 4);
$this->createOrder($customerId3, Carbon::now(), 'flat_rate', 1);
$this->createOrder($customerId3, Carbon::now(), 'free_shipping', 3);
$this->createOrder($customerId4, Carbon::now(), 'flat_rate', 4);
$this->assertFilterReturnsEmails('any', ['Flat rate'], 1, ['c1@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('any', ['Local pickup'], 1, ['c2@e.com']);
$this->assertFilterReturnsEmails('any', ['Nonexistent method'], 1000, []);
$this->assertFilterReturnsEmails('any', ['flat_rate:1'], 1, ['c1@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('any', ['local_pickup:2'], 1, ['c2@e.com']);
$this->assertFilterReturnsEmails('any', ['local_pickup:2', 'flat_rate:1'], 1, ['c1@e.com', 'c2@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('any', ['nonexistent_method:1'], 1000, []);
}
public function testItWorksWithAllOperator(): void {
$customerId1 = $this->tester->createCustomer('c1@e.com');
$this->createOrder($customerId1, Carbon::now(), 'Flat rate');
$this->createOrder($customerId1, Carbon::now(), 'Flat rate');
$this->createOrder($customerId1, Carbon::now(), 'flat_rate', 1);
$this->createOrder($customerId1, Carbon::now(), 'flat_rate', 1);
$customerId2 = $this->tester->createCustomer('c2@e.com');
$this->createOrder($customerId2, Carbon::now(), 'Free shipping');
$this->createOrder($customerId2, Carbon::now(), 'free_shipping', 2);
$customerId3 = $this->tester->createCustomer('c3@e.com');
$this->createOrder($customerId3, Carbon::now(), 'Free shipping');
$this->createOrder($customerId3, Carbon::now(), 'Flat rate');
$this->createOrder($customerId3, Carbon::now(), 'free_shipping', 2);
$this->createOrder($customerId3, Carbon::now(), 'flat_rate', 1);
$this->assertfilterreturnsemails('all', ['Flat rate'], 1, ['c1@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('all', ['Free shipping'], 1, ['c2@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('all', ['Free shipping', 'Flat rate'], 1, ['c3@e.com']);
$this->assertFilterReturnsEmails('all', ['Nonexistent method', 'Flat rate'], 1000, []);
$this->assertfilterreturnsemails('all', ['flat_rate:1'], 1, ['c1@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('all', ['free_shipping:2'], 1, ['c2@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('all', ['free_shipping:2', 'flat_rate:1'], 1, ['c3@e.com']);
$this->assertFilterReturnsEmails('all', ['nonexistent_method:1', 'flat_rate:1'], 1000, []);
}
public function testItWorksWithNoneOperator(): void {
$customerId1 = $this->tester->createCustomer('c1@e.com');
$this->createOrder($customerId1, Carbon::now(), 'Flat rate');
$this->createOrder($customerId1, Carbon::now(), 'Flat rate');
$this->createOrder($customerId1, Carbon::now(), 'flat_rate', 1);
$this->createOrder($customerId1, Carbon::now(), 'flat_rate', 1);
$customerId2 = $this->tester->createCustomer('c2@e.com');
$this->createOrder($customerId2, Carbon::now(), 'Free shipping');
$this->createOrder($customerId2, Carbon::now(), 'free_shipping', 2);
$customerId3 = $this->tester->createCustomer('c3@e.com');
$this->createOrder($customerId3, Carbon::now(), 'Free shipping');
$this->createOrder($customerId3, Carbon::now(), 'Flat rate');
$this->createOrder($customerId3, Carbon::now(), 'free_shipping', 2);
$this->createOrder($customerId3, Carbon::now(), 'flat_rate', 1);
(new Subscriber)->withEmail('sub@e.com')->create();
$this->assertFilterReturnsEmails('none', ['Flat rate'], 1, ['sub@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['Free shipping'], 1, ['sub@e.com', 'c1@e.com']);
$this->assertFilterReturnsEmails('none', ['Nonexistent method'], 1000, ['sub@e.com', 'c1@e.com', 'c2@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('none', ['Flat rate', 'Free shipping'], 1, ['sub@e.com']);
$this->assertFilterReturnsEmails('none', ['flat_rate:1'], 1, ['sub@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['free_shipping:2'], 1, ['sub@e.com', 'c1@e.com']);
$this->assertFilterReturnsEmails('none', ['nonexistent_method:1'], 1000, ['sub@e.com', 'c1@e.com', 'c2@e.com', 'c3@e.com']);
$this->assertFilterReturnsEmails('none', ['flat_rate:1', 'free_shipping:2'], 1, ['sub@e.com']);
}
public function testItWorksWithDateRanges(): void {
$customerId1 = $this->tester->createCustomer('c1@e.com');
$this->createOrder($customerId1, Carbon::now()->subDays(2)->addMinute(), 'Flat rate');
$this->createOrder($customerId1, Carbon::now()->subDays(5)->addMinute(), 'Free shipping');
$this->createOrder($customerId1, Carbon::now()->subDays(2)->addMinute(), 'flat_rate', 1);
$this->createOrder($customerId1, Carbon::now()->subDays(5)->addMinute(), 'free_shipping', 2);
$customerId2 = $this->tester->createCustomer('c2@e.com');
$this->createOrder($customerId2, Carbon::now()->subDays(100)->addMinute(), 'Local pickup');
$this->assertFilterReturnsEmails('any', ['Flat rate'], 1, []);
$this->assertFilterReturnsEmails('any', ['Flat rate'], 2, ['c1@e.com']);
$this->assertFilterReturnsEmails('any', ['Free shipping'], 4, []);
$this->assertFilterReturnsEmails('any', ['Free shipping'], 5, ['c1@e.com']);
$this->assertFilterReturnsEmails('any', ['Local pickup'], 99, []);
$this->assertFilterReturnsEmails('any', ['Local pickup'], 100, ['c2@e.com']);
$this->assertFilterReturnsEmails('any', ['Local pickup', 'Flat rate'], 100, ['c1@e.com', 'c2@e.com']);
$this->createOrder($customerId2, Carbon::now()->subDays(100)->addMinute(), 'local_pickup', 3);
$this->assertFilterReturnsEmails('any', ['flat_rate:1'], 1, []);
$this->assertFilterReturnsEmails('any', ['flat_rate:1'], 2, ['c1@e.com']);
$this->assertFilterReturnsEmails('any', ['free_shipping:2'], 4, []);
$this->assertFilterReturnsEmails('any', ['free_shipping:2'], 5, ['c1@e.com']);
$this->assertFilterReturnsEmails('any', ['local_pickup:3'], 99, []);
$this->assertFilterReturnsEmails('any', ['local_pickup:3'], 100, ['c2@e.com']);
$this->assertFilterReturnsEmails('any', ['local_pickup:3', 'flat_rate:1'], 100, ['c1@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('all', ['Flat rate'], 1, []);
$this->assertFilterReturnsEmails('all', ['Flat rate'], 2, ['c1@e.com']);
$this->assertFilterReturnsEmails('all', ['Flat rate', 'Free shipping'], 2, []);
$this->assertFilterReturnsEmails('all', ['Flat rate', 'Free shipping'], 5, ['c1@e.com']);
$this->assertFilterReturnsEmails('all', ['flat_rate:1'], 1, []);
$this->assertFilterReturnsEmails('all', ['flat_rate:1'], 2, ['c1@e.com']);
$this->assertFilterReturnsEmails('all', ['flat_rate:1', 'free_shipping:2'], 2, []);
$this->assertFilterReturnsEmails('all', ['flat_rate:1', 'free_shipping:2'], 5, ['c1@e.com']);
$this->assertFilterReturnsEmails('none', ['Flat rate'], 1, ['c1@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['Flat rate'], 2, ['c2@e.com']);
$this->assertFilterReturnsEmails('none', ['Free shipping'], 2, ['c1@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['Free shipping'], 5, ['c2@e.com']);
$this->assertFilterReturnsEmails('none', ['flat_rate:1'], 1, ['c1@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['flat_rate:1'], 2, ['c2@e.com']);
$this->assertFilterReturnsEmails('none', ['free_shipping:2'], 2, ['c1@e.com', 'c2@e.com']);
$this->assertFilterReturnsEmails('none', ['free_shipping:2'], 5, ['c2@e.com']);
}
private function assertFilterReturnsEmails(string $operator, array $shippingMethods, int $days, array $expectedEmails): void {
private function assertFilterReturnsEmails(string $operator, array $shippingMethodStrings, int $days, array $expectedEmails): void {
$filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommerceUsedShippingMethod::ACTION, [
'operator' => $operator,
'shipping_methods' => $shippingMethods,
'shipping_methods' => $shippingMethodStrings,
'used_shipping_method_days' => $days,
]);
$emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($filterData, $this->filter);
$this->assertEqualsCanonicalizing($expectedEmails, $emails);
}
private function createOrder(int $customerId, Carbon $createdAt, string $shippingMethodTitle): int {
private function createOrder(int $customerId, Carbon $createdAt, string $shippingMethodId, $shippingInstanceId): int {
$order = $this->tester->createWooCommerceOrder();
$order->set_customer_id($customerId);
$order->set_date_created($createdAt->toDateTimeString());
$order->set_status('wc-completed');
$shippingItem = new \WC_Order_Item_Shipping();
$shippingItem->set_method_title($shippingMethodTitle);
$shippingItem->set_method_id($shippingMethodId);
$shippingItem->set_instance_id($shippingInstanceId);
$order->add_item($shippingItem);
$order->save();