diff --git a/lib/Segments/DynamicSegments/Filters/WooCommerceCountry.php b/lib/Segments/DynamicSegments/Filters/WooCommerceCountry.php index 5c25363036..4a9f65442a 100644 --- a/lib/Segments/DynamicSegments/Filters/WooCommerceCountry.php +++ b/lib/Segments/DynamicSegments/Filters/WooCommerceCountry.php @@ -5,6 +5,7 @@ namespace MailPoet\Segments\DynamicSegments\Filters; use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SubscriberEntity; +use MailPoet\Util\DBCollationChecker; use MailPoet\Util\Security; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; use MailPoetVendor\Doctrine\ORM\EntityManager; @@ -15,10 +16,15 @@ class WooCommerceCountry implements Filter { /** @var EntityManager */ private $entityManager; + /** @var DBCollationChecker */ + private $collationChecker; + public function __construct( - EntityManager $entityManager + EntityManager $entityManager, + DBCollationChecker $collationChecker ) { $this->entityManager = $entityManager; + $this->collationChecker = $collationChecker; } public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { @@ -34,21 +40,24 @@ class WooCommerceCountry implements Filter { } $countryFilterParam = 'countryCode' . $filter->getId() ?? Security::generateRandomString(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $collation = $this->collationChecker->getCollateIfNeeded( + $subscribersTable, + 'email', + $wpdb->prefix . 'wc_customer_lookup', + 'email' + ); return $queryBuilder->innerJoin( $subscribersTable, - $wpdb->postmeta, - 'postmeta', - "postmeta.meta_key = '_customer_user' AND $subscribersTable.wp_user_id=postmeta.meta_value" + $wpdb->prefix . 'wc_customer_lookup', + 'customer', + "$subscribersTable.email = customer.email $collation" )->innerJoin( - 'postmeta', - $wpdb->posts, - 'posts', - "postmeta.post_id = posts.id AND posts.post_status NOT IN ('wc-cancelled', 'wc-failed')" - )->innerJoin( - 'postmeta', - $wpdb->postmeta, - 'postmetaCountry', - "postmeta.post_id = postmetaCountry.post_id AND postmetaCountry.meta_key = '_billing_country' AND postmetaCountry.meta_value = :$countryFilterParam" - )->setParameter($countryFilterParam, $countryCode[0]); + 'customer', + $wpdb->prefix . 'wc_order_stats', + 'orderStats', + 'customer.customer_id = orderStats.customer_id' + )->where("customer.country = :$countryFilterParam") + ->andWhere('orderStats.status NOT IN ("wc-cancelled", "wc-failed")') + ->setParameter($countryFilterParam, $countryCode[0]); } } diff --git a/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCountryTest.php b/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCountryTest.php index 79494a30f1..8544f521f7 100644 --- a/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCountryTest.php +++ b/tests/integration/Segments/DynamicSegments/Filters/WooCommerceCountryTest.php @@ -7,8 +7,7 @@ use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Subscribers\SubscribersRepository; -use MailPoet\WP\Functions as WPFunctions; -use MailPoetVendor\Doctrine\DBAL\Driver\Statement; +use MailPoetVendor\Doctrine\DBAL\ForwardCompatibility\DriverStatement; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; class WooCommerceCountryTest extends \MailPoetTest { @@ -16,18 +15,12 @@ class WooCommerceCountryTest extends \MailPoetTest { /** @var WooCommerceCountry */ private $wooCommerceCountry; - /** @var WPFunctions */ - private $wp; - - /** @var int[] */ - private $orders; - /** @var SubscribersRepository */ private $subscribersRepository; public function _before(): void { + self::createLookUpTables(); $this->wooCommerceCountry = $this->diContainer->get(WooCommerceCountry::class); - $this->wp = $this->diContainer->get(WPFunctions::class); $this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class); $this->cleanup(); @@ -36,21 +29,22 @@ class WooCommerceCountryTest extends \MailPoetTest { $userId2 = $this->tester->createWordPressUser('customer2@example.com', 'customer'); $userId3 = $this->tester->createWordPressUser('customer3@example.com', 'customer'); - $this->orders[] = $this->createOrder(['user_id' => $userId1, 'billing_country' => 'US']); - $this->orders[] = $this->createOrder(['user_id' => $userId2, 'billing_country' => 'US']); - $this->orders[] = $this->createOrder(['user_id' => $userId3]); + $this->createCustomerLookupData(['user_id' => $userId1, 'email' => 'customer1@example.com', 'country' => 'CZ']); + $this->createCustomerLookupData(['user_id' => $userId2, 'email' => 'customer2@example.com', 'country' => 'US']); + $this->createCustomerLookupData(['user_id' => $userId3, 'email' => 'customer3@example.com', 'country' => 'US']); } public function testItAppliesFilter(): void { $segmentFilter = $this->getSegmentFilter('CZ'); $queryBuilder = $this->wooCommerceCountry->apply($this->getQueryBuilder(), $segmentFilter); $statement = $queryBuilder->execute(); - $result = $statement instanceof Statement ? $statement->fetchAll() : []; + assert($statement instanceof DriverStatement); + $result = $statement->fetchAll(); expect(count($result))->equals(1); $subscriber1 = $this->subscribersRepository->findOneById($result[0]['inner_subscriber_id']); assert($subscriber1 instanceof SubscriberEntity); expect($subscriber1)->isInstanceOf(SubscriberEntity::class); - expect($subscriber1->getEmail())->equals('customer3@example.com'); + expect($subscriber1->getEmail())->equals('customer1@example.com'); } private function getQueryBuilder(): QueryBuilder { @@ -74,17 +68,40 @@ class WooCommerceCountryTest extends \MailPoetTest { return $dynamicSegmentFilter; } - private function createOrder($data): int { - return (int)$this->wp->wpInsertPost([ - 'post_type' => 'shop_order', - 'post_status' => 'wc-completed', - 'post_date' => $data['post_date'] ?? '', - 'meta_input' => [ - '_customer_user' => $data['user_id'] ?? '', - '_order_total' => $data['order_total'] ?? '1', - '_billing_country' => $data['billing_country'] ?? 'CZ', - ], - ]); + private function createCustomerLookupData(array $data) { + $connection = $this->entityManager->getConnection(); + global $wpdb; + $customerLookupTable = $wpdb->prefix . 'wc_customer_lookup'; + $connection->executeQuery(" + INSERT INTO {$customerLookupTable} (user_id, first_name, last_name, email, country) + VALUES ( + {$data['user_id']}, + '', + '', + '{$data['email']}', + '{$data['country']}' + ) + "); + $id = $connection->lastInsertId(); + $orderId = (int)$id + 1; + $orderLookupTable = $wpdb->prefix . 'wc_order_stats'; + $connection->executeQuery(" + INSERT INTO {$orderLookupTable} (order_id, status, customer_id) + VALUES ( + {$orderId}, + 'wc-completed', + {$id} + ) + "); + } + + private function cleanUpLookUpTables(): void { + $connection = $this->entityManager->getConnection(); + global $wpdb; + $lookupTable = $wpdb->prefix . 'wc_customer_lookup'; + $orderLookupTable = $wpdb->prefix . 'wc_order_stats'; + $connection->executeStatement("TRUNCATE $lookupTable"); + $connection->executeStatement("TRUNCATE $orderLookupTable"); } public function _after(): void { @@ -99,8 +116,152 @@ class WooCommerceCountryTest extends \MailPoetTest { foreach ($emails as $email) { $this->tester->deleteWordPressUser($email); } - foreach ($this->orders ?? [] as $orderId) { - $this->wp->wpDeletePost($orderId); + $this->cleanUpLookUpTables(); + } + + /** + * Get WC Lookup Tables database schema. + * Copied from WC-Admin version 2.9.2-plugin + * + * @return string + */ + protected static function getSchema(): string { + global $wpdb; + + $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : ''; + + // Max DB index length. See wp_get_db_schema(). + $maxIndexLength = 191; + + return " + CREATE TABLE {$wpdb->prefix}wc_order_stats ( + order_id bigint(20) unsigned NOT NULL, + parent_id bigint(20) unsigned DEFAULT 0 NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + num_items_sold int(11) DEFAULT 0 NOT NULL, + total_sales double DEFAULT 0 NOT NULL, + tax_total double DEFAULT 0 NOT NULL, + shipping_total double DEFAULT 0 NOT NULL, + net_total double DEFAULT 0 NOT NULL, + returning_customer boolean DEFAULT NULL, + status varchar(200) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_id), + KEY date_created (date_created), + KEY customer_id (customer_id), + KEY status (status({$maxIndexLength})) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_order_product_lookup ( + order_item_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + variation_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + product_qty INT NOT NULL, + product_net_revenue double DEFAULT 0 NOT NULL, + product_gross_revenue double DEFAULT 0 NOT NULL, + coupon_amount double DEFAULT 0 NOT NULL, + tax_amount double DEFAULT 0 NOT NULL, + shipping_amount double DEFAULT 0 NOT NULL, + shipping_tax_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id), + KEY product_id (product_id), + KEY customer_id (customer_id), + KEY date_created (date_created) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + shipping_tax double DEFAULT 0 NOT NULL, + order_tax double DEFAULT 0 NOT NULL, + total_tax double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, tax_rate_id), + KEY tax_rate_id (tax_rate_id), + KEY date_created (date_created) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + coupon_id BIGINT NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + discount_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, coupon_id), + KEY coupon_id (coupon_id), + KEY date_created (date_created) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_admin_notes ( + note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + type varchar(20) NOT NULL, + locale varchar(20) NOT NULL, + title longtext NOT NULL, + content longtext NOT NULL, + content_data longtext NULL default null, + status varchar(200) NOT NULL, + source varchar(200) NOT NULL, + date_created datetime NOT NULL default '0000-00-00 00:00:00', + date_reminder datetime NULL default null, + is_snoozable boolean DEFAULT 0 NOT NULL, + layout varchar(20) DEFAULT '' NOT NULL, + image varchar(200) NULL DEFAULT NULL, + is_deleted boolean DEFAULT 0 NOT NULL, + is_read boolean DEFAULT 0 NOT NULL, + icon varchar(200) NOT NULL default 'info', + PRIMARY KEY (note_id) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_admin_note_actions ( + action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + note_id BIGINT UNSIGNED NOT NULL, + name varchar(255) NOT NULL, + label varchar(255) NOT NULL, + query longtext NOT NULL, + status varchar(255) NOT NULL, + is_primary boolean DEFAULT 0 NOT NULL, + actioned_text varchar(255) NOT NULL, + nonce_action varchar(255) NULL DEFAULT NULL, + nonce_name varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (action_id), + KEY note_id (note_id) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( + customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED DEFAULT NULL, + username varchar(60) DEFAULT '' NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255) NOT NULL, + email varchar(100) NULL default NULL, + date_last_active timestamp NULL default null, + date_registered timestamp NULL default null, + country char(2) DEFAULT '' NOT NULL, + postcode varchar(20) DEFAULT '' NOT NULL, + city varchar(100) DEFAULT '' NOT NULL, + state varchar(100) DEFAULT '' NOT NULL, + PRIMARY KEY (customer_id), + UNIQUE KEY user_id (user_id), + KEY email (email) + ) $collate; + CREATE TABLE {$wpdb->prefix}wc_category_lookup ( + category_tree_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (category_tree_id,category_id) + ) $collate; + "; + } + + /** + * Create WC Lookup database tables. + */ + public static function createLookUpTables() { + if ((boolean)getenv('MULTISITE') === true) { + $wpUpgradePath = getenv('WP_ROOT_MULTISITE'); + } else { + $wpUpgradePath = getenv('WP_ROOT'); } + require_once($wpUpgradePath . '/wp-admin/includes/upgrade.php'); + + dbDelta( self::getSchema() ); } }