Add support for local variation attributes

In WooCommerce it's possible to create attributes that are local to a
specific product. When these attributes are used to generate variations,
 they are stored in the postmeta table.

MAILPOET-5467
This commit is contained in:
John Oleksowicz
2024-02-16 11:55:28 -06:00
committed by Aschepikov
parent 24e38d6752
commit a4a2ef3b1f
10 changed files with 521 additions and 90 deletions

View File

@@ -9,6 +9,7 @@ import {
AnyValueTypes,
FilterProps,
SelectOption,
WindowLocalProductAttributes,
WindowProductAttributes,
WooCommerceFormItem,
} from '../../../types';
@@ -18,9 +19,15 @@ export function validatePurchasedWithAttribute(
): boolean {
const purchasedProductWithAttributeIsInvalid =
!formItems.operator ||
formItems.attribute_taxonomy_slug === undefined ||
!Array.isArray(formItems.attribute_term_ids) === undefined ||
formItems.attribute_term_ids.length === 0;
(formItems.attribute_type === 'taxonomy' &&
(formItems.attribute_taxonomy_slug === undefined ||
!Array.isArray(formItems.attribute_term_ids) ||
formItems.attribute_term_ids.length === 0)) ||
(formItems.attribute_type === 'local' &&
(!formItems.attribute_local_name ||
formItems.attribute_local_name.length === 0 ||
!Array.isArray(formItems.attribute_local_values) ||
formItems.attribute_local_values.length === 0));
return !purchasedProductWithAttributeIsInvalid;
}
@@ -46,21 +53,61 @@ export function PurchasedWithAttributeFields({
}),
);
const localProductAttributes: WindowLocalProductAttributes = useSelect(
(select) => select(storeName).getLocalProductAttributes(),
[],
);
const localAttributeOptions = Object.values(localProductAttributes).map(
(attribute) => ({
// Appending @local to avoid conflicts between taxonomy and local attributes with the same name
value: `${attribute.name}@local`,
label: attribute.name,
}),
);
const localAttributeValues = Object.values(localAttributeOptions).map(
(option) => option.value,
);
const combinedOptions = [
...productAttributesOptions,
...localAttributeOptions,
];
const productAttributeTermsOptionsRef = useRef(null);
useEffect(() => {
if (segment.attribute_taxonomy_slug === undefined) {
if (
segment.attribute_taxonomy_slug === undefined &&
segment.attribute_local_name === undefined
) {
productAttributeTermsOptionsRef.current = null;
return;
}
productAttributeTermsOptionsRef.current = productAttributes[
segment.attribute_taxonomy_slug
].terms.map((term) => ({
value: term.term_id.toString(),
label: term.name,
}));
}, [segment.attribute_taxonomy_slug, productAttributes]);
if (segment.attribute_type === 'taxonomy') {
productAttributeTermsOptionsRef.current = productAttributes[
segment.attribute_taxonomy_slug
].terms.map((term) => ({
value: term.term_id.toString(),
label: term.name,
}));
} else if (segment.attribute_type === 'local') {
productAttributeTermsOptionsRef.current = localProductAttributes[
segment.attribute_local_name
].values.map((value) => ({
value,
label: value,
}));
}
}, [
segment.attribute_taxonomy_slug,
segment.attribute_type,
segment.attribute_local_name,
productAttributes,
localProductAttributes,
]);
useEffect(() => {
if (
@@ -90,23 +137,52 @@ export function PurchasedWithAttributeFields({
dimension="small"
key="select-segment-product-attribute"
placeholder={__('Search attributes', 'mailpoet')}
options={productAttributesOptions}
value={filter((productAttributeOption) => {
if (segment.attribute_taxonomy_slug === undefined) {
return undefined;
}
return (
segment.attribute_taxonomy_slug === productAttributeOption.value
);
}, productAttributesOptions)}
options={combinedOptions}
value={
segment.attribute_type === 'local'
? filter((localAttributeOption) => {
if (!segment.attribute_local_name) {
return undefined;
}
return (
`${segment.attribute_local_name}@local` ===
localAttributeOption.value
);
}, localAttributeOptions)
: filter((productAttributeOption) => {
if (segment.attribute_taxonomy_slug === undefined) {
return undefined;
}
return (
segment.attribute_taxonomy_slug ===
productAttributeOption.value
);
}, productAttributesOptions)
}
onChange={(option: SelectOption): void => {
void updateSegmentFilter(
{
attribute_taxonomy_slug: option.value,
attribute_term_ids: [],
},
filterIndex,
);
if (localAttributeValues.includes(option.value)) {
void updateSegmentFilter(
{
attribute_type: 'local',
attribute_local_name: option.value.replace(/@local$/, ''),
attribute_local_values: [],
attribute_taxonomy_slug: null,
attribute_term_ids: null,
},
filterIndex,
);
} else {
void updateSegmentFilter(
{
attribute_type: 'taxonomy',
attribute_local_name: null,
attribute_local_values: null,
attribute_taxonomy_slug: option.value,
attribute_term_ids: [],
},
filterIndex,
);
}
}}
/>
{productAttributeTermsOptionsRef.current && (
@@ -118,26 +194,46 @@ export function PurchasedWithAttributeFields({
options={productAttributeTermsOptionsRef.current}
value={filter(
(productAttributeTermOption: { value: string; label: string }) => {
if (segment.attribute_term_ids === undefined) {
return undefined;
if (segment.attribute_local_values) {
return (
segment.attribute_local_values.indexOf(
productAttributeTermOption.value,
) !== -1
);
}
return (
segment.attribute_term_ids.indexOf(
productAttributeTermOption.value,
) !== -1
);
if (segment.attribute_term_ids) {
return (
segment.attribute_term_ids.indexOf(
productAttributeTermOption.value,
) !== -1
);
}
return undefined;
},
productAttributeTermsOptionsRef.current,
)}
onChange={(options: SelectOption[]): void => {
void updateSegmentFilter(
{
attribute_term_ids: (options || []).map(
(x: SelectOption) => x.value,
),
},
filterIndex,
);
if (segment.attribute_type === 'local') {
void updateSegmentFilter(
{
attribute_term_ids: null,
attribute_local_values: (options || []).map(
(x: SelectOption) => x.value,
),
},
filterIndex,
);
} else {
void updateSegmentFilter(
{
attribute_term_ids: (options || []).map(
(x: SelectOption) => x.value,
),
attribute_local_values: null,
},
filterIndex,
);
}
}}
/>
)}

View File

@@ -28,6 +28,7 @@ export const getInitialState = (): StateType => ({
membershipPlans: window.mailpoet_membership_plans,
subscriptionProducts: window.mailpoet_subscription_products,
productAttributes: window.mailpoet_product_attributes,
localProductAttributes: window.mailpoet_local_product_attributes,
productCategories: window.mailpoet_product_categories,
newslettersList: window.mailpoet_newsletters_list,
wordpressRoles: window.wordpress_editable_roles_list,

View File

@@ -12,6 +12,7 @@ import {
Tag,
WindowCustomFields,
WindowEditableRoles,
WindowLocalProductAttributes,
WindowMembershipPlans,
WindowNewslettersList,
WindowProductAttributes,
@@ -34,6 +35,9 @@ export const getWordpressRoles = (state: StateType): WindowEditableRoles =>
export const getProductAttributes = (
state: StateType,
): WindowProductAttributes => state.productAttributes;
export const getLocalProductAttributes = (
state: StateType,
): WindowLocalProductAttributes => state.localProductAttributes;
export const getProductCategories = (
state: StateType,
): WindowProductCategories => state.productCategories;

View File

@@ -152,8 +152,11 @@ export interface WooCommerceFormItem extends FormItem {
count?: string;
days?: string;
coupon_code_ids?: string[];
attribute_type?: 'local' | 'taxonomy';
attribute_taxonomy_slug?: string;
attribute_term_ids?: string[];
attribute_local_name?: string;
attribute_local_values?: string[];
}
export interface AutomationsFormItem extends FormItem {
@@ -235,6 +238,11 @@ export type WindowProductAttributes = {
terms: [];
}[];
export type WindowLocalProductAttributes = {
name: string;
values: string[];
}[];
export type WindowProductCategories = {
id: string;
name: string;
@@ -274,6 +282,7 @@ export interface SegmentFormDataWindow extends Window {
mailpoet_membership_plans: WindowMembershipPlans;
mailpoet_subscription_products: WindowSubscriptionProducts;
mailpoet_product_attributes: WindowProductAttributes;
mailpoet_local_product_attributes: WindowLocalProductAttributes;
mailpoet_product_categories: WindowProductCategories;
mailpoet_woocommerce_countries: WindowWooCommerceCountries;
mailpoet_woocommerce_payment_methods: WooPaymentMethod[];
@@ -295,6 +304,7 @@ export interface StateType {
subscriptionProducts: WindowSubscriptionProducts;
wordpressRoles: WindowEditableRoles;
productAttributes: WindowProductAttributes;
localProductAttributes: WindowLocalProductAttributes;
productCategories: WindowProductCategories;
newslettersList: WindowNewslettersList;
canUseWooMemberships: boolean;

View File

@@ -148,6 +148,16 @@ class DynamicSegments {
];
}
}
// Fetch local attributes used for product variations
$data['local_product_attributes'] = [];
$localAttributes = $this->getLocalAttributesUsedInProductVariations();
foreach ($localAttributes as $localAttribute => $values) {
$data['local_product_attributes'][$localAttribute] = [
'name' => $localAttribute,
'values' => $values,
];
}
}
$data['product_categories'] = $this->wpPostListLoader->getWooCommerceCategories();
@@ -204,6 +214,35 @@ class DynamicSegments {
$this->pageRenderer->displayPage('segments/dynamic.html', $data);
}
private function getLocalAttributesUsedInProductVariations(): array {
$attributes = [];
if (!$this->woocommerceHelper->isWooCommerceActive()) {
return $attributes;
}
global $wpdb;
$query = "
SELECT DISTINCT pm.meta_key, pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key LIKE 'attribute_%'
AND p.post_type = 'product_variation'
GROUP BY pm.meta_key, pm.meta_value";
$results = $wpdb->get_results($query, ARRAY_A);
foreach ($results as $result) {
$attribute = substr($result['meta_key'], 10);
if (!isset($attributes[$attribute])) {
$attributes[$attribute] = [];
}
$attributes[$attribute][] = $result['meta_value'];
}
return $attributes;
}
private function getNewslettersList(): array {
$result = [];
foreach ($this->newslettersRepository->getStandardNewsletterList() as $newsletter) {

View File

@@ -519,6 +519,9 @@ class FilterDataMapper {
$filterData['operator'] = $data['operator'];
$filterData['attribute_taxonomy_slug'] = $data['attribute_taxonomy_slug'];
$filterData['attribute_term_ids'] = $data['attribute_term_ids'];
$filterData['attribute_type'] = $data['attribute_type'];
$filterData['attribute_local_name'] = $data['attribute_local_name'];
$filterData['attribute_local_values'] = $data['attribute_local_values'];
} else {
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
}

View File

@@ -12,6 +12,9 @@ use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
class WooCommercePurchasedWithAttribute implements Filter {
const ACTION = 'purchasedWithAttribute';
const TYPE_LOCAL = 'local';
const TYPE_TAXONOMY = 'taxonomy';
private WooFilterHelper $wooFilterHelper;
private FilterHelper $filterHelper;
@@ -32,34 +35,24 @@ class WooCommercePurchasedWithAttribute implements Filter {
$filterData = $filter->getFilterData();
$this->validateFilterData((array)$filterData->getData());
$operator = $filterData->getOperator();
$attributeTaxonomySlug = $filterData->getStringParam('attribute_taxonomy_slug');
$attributeTermIds = $filterData->getArrayParam('attribute_term_ids');
$type = $filterData->getStringParam('attribute_type');
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
$this->applyForAnyOperator($queryBuilder, $attributeTaxonomySlug, $attributeTermIds);
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
$this->applyForAnyOperator($queryBuilder, $attributeTaxonomySlug, $attributeTermIds);
$countParam = $this->filterHelper->getUniqueParameterName('count');
$queryBuilder
->groupBy('inner_subscriber_id')
->having("COUNT(DISTINCT attribute.term_id) = :$countParam")
->setParameter($countParam, count($attributeTermIds));
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
$this->applyForAnyOperator($subQuery, $attributeTaxonomySlug, $attributeTermIds);
$subscribersTable = $this->filterHelper->getSubscribersTable();
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
if ($type === self::TYPE_LOCAL) {
$this->applyForLocalAttribute($queryBuilder, $filterData);
} elseif ($type === self::TYPE_TAXONOMY) {
$this->applyForTaxonomyAttribute($queryBuilder, $filterData);
}
return $queryBuilder;
}
private function applyForAnyOperator(QueryBuilder $queryBuilder, string $attributeTaxonomySlug, array $attributeTermIds): void {
private function applyForTaxonomyAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
$attributeTaxonomySlug = $filterData->getStringParam('attribute_taxonomy_slug');
$attributeTermIds = $filterData->getArrayParam('attribute_term_ids');
$termIdsParam = $this->filterHelper->getUniqueParameterName('attribute_term_ids');
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
$attributeAlias = $this->applyAttributeJoin($queryBuilder, $productAlias, $attributeTaxonomySlug);
$attributeAlias = $this->applyTaxonomyAttributeJoin($queryBuilder, $productAlias, $attributeTaxonomySlug);
$queryBuilder->andWhere("$attributeAlias.term_id IN (:$termIdsParam)");
$queryBuilder->setParameter($termIdsParam, $attributeTermIds, Connection::PARAM_STR_ARRAY);
}
@@ -74,7 +67,7 @@ class WooCommercePurchasedWithAttribute implements Filter {
return $alias;
}
private function applyAttributeJoin(QueryBuilder $queryBuilder, string $productAlias, $taxonomySlug, string $alias = 'attribute'): string {
private function applyTaxonomyAttributeJoin(QueryBuilder $queryBuilder, string $productAlias, $taxonomySlug, string $alias = 'attribute'): string {
$queryBuilder->innerJoin(
$productAlias,
$this->filterHelper->getPrefixedTable('wc_product_attributes_lookup'),
@@ -86,6 +79,12 @@ class WooCommercePurchasedWithAttribute implements Filter {
}
public function getLookupData(DynamicSegmentFilterData $filterData): array {
$type = $filterData->getStringParam('attribute_type');
if ($type !== self::TYPE_TAXONOMY) {
return [];
}
$slug = $filterData->getStringParam('attribute_taxonomy_slug');
$lookupData = [
@@ -117,12 +116,93 @@ class WooCommercePurchasedWithAttribute implements Filter {
) {
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
}
$attribute_taxonomy_slug = $data['attribute_taxonomy_slug'] ?? null;
if (!is_string($attribute_taxonomy_slug) || strlen($attribute_taxonomy_slug) === 0) {
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
$this->validateAttributeData($data);
}
public function validateAttributeData(array $data): void {
$type = $data['attribute_type'];
if (!in_array($type, [self::TYPE_LOCAL, self::TYPE_TAXONOMY], true)) {
throw new InvalidFilterException('Invalid attribute type', InvalidFilterException::INVALID_TYPE);
}
if (!isset($data['attribute_term_ids']) || !is_array($data['attribute_term_ids']) || count($data['attribute_term_ids']) === 0) {
throw new InvalidFilterException('Missing attribute terms', InvalidFilterException::MISSING_VALUE);
if ($type === self::TYPE_LOCAL) {
$name = $data['attribute_local_name'] ?? null;
if (!is_string($name) || strlen($name) === 0) {
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
}
$values = $data['attribute_local_values'] ?? [];
if (!is_array($values) || count($values) === 0) {
throw new InvalidFilterException('Missing attribute values', InvalidFilterException::MISSING_VALUE);
}
}
if ($type === self::TYPE_TAXONOMY) {
$attribute_taxonomy_slug = $data['attribute_taxonomy_slug'] ?? null;
if (!is_string($attribute_taxonomy_slug) || strlen($attribute_taxonomy_slug) === 0) {
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
}
if (!isset($data['attribute_term_ids']) || !is_array($data['attribute_term_ids']) || count($data['attribute_term_ids']) === 0) {
throw new InvalidFilterException('Missing attribute terms', InvalidFilterException::MISSING_VALUE);
}
}
}
private function applyForTaxonomyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData) {
$operator = $filterData->getOperator();
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
$countParam = $this->filterHelper->getUniqueParameterName('count');
$queryBuilder
->groupBy('inner_subscriber_id')
->having("COUNT(DISTINCT attribute.term_id) = :$countParam")
->setParameter($countParam, count($filterData->getArrayParam('attribute_term_ids')));
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
$this->applyForTaxonomyAnyOperator($subQuery, $filterData);
$subscribersTable = $this->filterHelper->getSubscribersTable();
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
}
}
private function applyForLocalAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
$operator = $filterData->getOperator();
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
$countParam = $this->filterHelper->getUniqueParameterName('count');
$queryBuilder
->groupBy('inner_subscriber_id')
->having("COUNT(DISTINCT postmeta.meta_value) = :$countParam")
->setParameter($countParam, count($filterData->getArrayParam('attribute_local_values')));
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
$this->applyForLocalAnyAttribute($subQuery, $filterData);
$subscribersTable = $this->filterHelper->getSubscribersTable();
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
}
}
private function applyForLocalAnyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
$attributeName = $filterData->getStringParam('attribute_local_name');
$attributeValues = $filterData->getArrayParam('attribute_local_values');
$valuesParam = $this->filterHelper->getUniqueParameterName('attribute_values');
$keyParam = $this->filterHelper->getUniqueParameterName('attribute_name');
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
$queryBuilder->innerJoin(
$productAlias,
$this->filterHelper->getPrefixedTable('postmeta'),
'postmeta',
"$productAlias.product_id = postmeta.post_id AND postmeta.meta_key = :$keyParam AND postmeta.meta_value IN (:$valuesParam)"
);
$queryBuilder->setParameter($keyParam, sprintf("attribute_%s", $attributeName));
$queryBuilder->setParameter($valuesParam, $attributeValues, Connection::PARAM_STR_ARRAY);
}
}

View File

@@ -161,6 +161,12 @@ class IntegrationTester extends \Codeception\Actor {
do_action('woocommerce_run_product_attribute_lookup_update_callback', $product->get_id(), 1);
}
if (isset($data['local_attributes'])) {
foreach ($data['local_attributes'] as $name => $value) {
update_post_meta($product->get_id(), sprintf('attribute_%s', $name), $value);
}
}
$this->wooProductIds[] = $product->get_id();
return $product;
}

View File

@@ -11,14 +11,13 @@ use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
*/
class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
private WooCommercePurchasedWithAttribute $filter;
public function _before(): void {
$this->filter = $this->diContainer->get(WooCommercePurchasedWithAttribute::class);
}
public function testItWorksWithAnyOperator(): void {
public function testItWorksWithAnyOperatorForTaxonomies(): void {
$product1 = $this->tester->createWooCommerceProduct([
'price' => 20,
'attributes' => [
@@ -41,10 +40,10 @@ class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
$this->createOrder($customer1, [$product1]);
$this->createOrder($customer2, [$product2]);
$this->assertFilterReturnsEmails('any', 'pa_color', [$blueTermId, $redTermId], ['customer1@example.com', 'customer2@example.com']);
$this->assertFilterReturnsEmailsForTaxonomyAttributes('any', 'pa_color', [$blueTermId, $redTermId], ['customer1@example.com', 'customer2@example.com']);
}
public function testItWorksWithNoneOperator(): void {
public function testItWorksWithNoneOperatorForTaxonomies(): void {
$product1 = $this->tester->createWooCommerceProduct([
'price' => 20,
'attributes' => [
@@ -67,10 +66,10 @@ class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
$this->createOrder($customer1, [$product1]);
$this->createOrder($customer2, [$product2]);
$this->assertFilterReturnsEmails('none', 'pa_color', [$blueTermId, $redTermId], ['customer3@example.com']);
$this->assertFilterReturnsEmailsForTaxonomyAttributes('none', 'pa_color', [$blueTermId, $redTermId], ['customer3@example.com']);
}
public function testItWorksWithAllOperator(): void {
public function testItWorksWithAllOperatorForTaxonomies(): void {
$product1 = $this->tester->createWooCommerceProduct([
'price' => 20,
'attributes' => [
@@ -93,7 +92,88 @@ class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
$this->createOrder($customer1, [$product1, $product2]);
$this->createOrder($customer2, [$product2]);
$this->assertFilterReturnsEmails('all', 'pa_color', [$blueTermId, $redTermId], ['customer1@example.com']);
$this->assertFilterReturnsEmailsForTaxonomyAttributes('all', 'pa_color', [$blueTermId, $redTermId], ['customer1@example.com']);
}
public function testItWorksWithAnyOperatorForLocalAttributes(): void {
$product1 = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'red',
],
]);
$product2 = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'blue',
],
]);
$customer1 = $this->tester->createCustomer('customer1@example.com');
$customer2 = $this->tester->createCustomer('customer2@example.com');
$customer3 = $this->tester->createCustomer('customer3@example.com');
$this->createOrder($customer1, [$product1]);
$this->createOrder($customer2, [$product2]);
$this->assertFilterReturnsEmailsForLocalAttributes('any', 'color', ['red', 'blue'], ['customer1@example.com', 'customer2@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('any', 'color', ['red'], ['customer1@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('any', 'color', ['blue'], ['customer2@example.com']);
}
public function testItWorksWithAllOperatorForLocalAttributes(): void {
$product1 = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'red',
],
]);
$product2 = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'blue',
],
]);
$customer1 = $this->tester->createCustomer('customer1@example.com');
$customer2 = $this->tester->createCustomer('customer2@example.com');
$customer3 = $this->tester->createCustomer('customer3@example.com');
$this->createOrder($customer1, [$product1, $product2]);
$this->createOrder($customer2, [$product1]);
$this->createOrder($customer3, [$product2]);
$this->assertFilterReturnsEmailsForLocalAttributes('all', 'color', ['red', 'blue'], ['customer1@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('all', 'color', ['red'], ['customer1@example.com', 'customer2@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('all', 'color', ['blue'], ['customer1@example.com', 'customer3@example.com']);
}
public function testItWorksWithNoneOperatorForLocalAttributes(): void {
$redProduct = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'red',
],
]);
$blueProduct = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'blue',
],
]);
$customer1 = $this->tester->createCustomer('customer1@example.com');
$customer2 = $this->tester->createCustomer('customer2@example.com');
$customer3 = $this->tester->createCustomer('customer3@example.com');
$customer4 = $this->tester->createCustomer('customer4@example.com');
$this->createOrder($customer1, [$redProduct, $blueProduct]);
$this->createOrder($customer2, [$redProduct]);
$this->createOrder($customer3, [$blueProduct]);
$this->assertFilterReturnsEmailsForLocalAttributes('none', 'color', ['red', 'blue'], ['customer4@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('none', 'color', ['red'], ['customer3@example.com', 'customer4@example.com']);
$this->assertFilterReturnsEmailsForLocalAttributes('none', 'color', ['blue'], ['customer2@example.com', 'customer4@example.com']);
}
public function testItRetrievesLookupData(): void {
@@ -117,6 +197,7 @@ class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
'operator' => 'any',
'attribute_taxonomy_slug' => 'pa_color',
'attribute_term_ids' => [$blueTermId, $redTermId],
'attribute_type' => 'taxonomy',
]);
$lookupData = $this->filter->getLookupData($filterData);
@@ -127,32 +208,142 @@ class WooCommercePurchasedWithAttributeTest extends \MailPoetTest {
], $lookupData);
}
public function testItValidatesOperator(): void {
$this->expectException(InvalidFilterException::class);
$this->expectExceptionMessage('Missing operator');
$this->expectExceptionCode(InvalidFilterException::MISSING_OPERATOR);
$this->filter->validateFilterData(['operator' => '', 'attribute_taxonomy_slug' => 'pa_color', 'attribute_term_ids' => ['1']]);
public function testItDoesNotGenerateLookupDataForLocalAttributes(): void {
$redProduct = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'red',
],
]);
$blueProduct = $this->tester->createWooCommerceProduct([
'price' => 20,
'local_attributes' => [
'color' => 'blue',
],
]);
$filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommercePurchasedWithAttribute::ACTION, [
'operator' => 'any',
'attribute_local_name' => 'color',
'attribute_local_values' => ['red', 'blue'],
'attribute_type' => 'local',
]);
$lookupData = $this->filter->getLookupData($filterData);
$this->assertSame([], $lookupData);
}
public function testItValidatesAttribute(): void {
$this->expectException(InvalidFilterException::class);
$this->expectExceptionMessage('Missing attribute');
$this->expectExceptionCode(InvalidFilterException::MISSING_VALUE);
$this->filter->validateFilterData(['operator' => 'any', 'attribute_taxonomy_slug' => '', 'attribute_term_ids' => ['1']]);
/**
* @dataProvider filterDataProvider
*/
public function testItValidatesFilterData(array $data, bool $isValid): void {
if (!$isValid) {
$this->expectException(InvalidFilterException::class);
}
$this->filter->validateFilterData($data);
}
public function testItValidatesTerms(): void {
$this->expectException(InvalidFilterException::class);
$this->expectExceptionMessage('Missing attribute terms');
$this->expectExceptionCode(InvalidFilterException::MISSING_VALUE);
$this->filter->validateFilterData(['operator' => 'any', 'attribute_taxonomy_slug' => 'pa_color', 'attribute_term_ids' => []]);
public function filterDataProvider(): array {
return [
'missing term ids' =>
[
[
'operator' => 'any',
'attribute_type' => 'taxonomy',
'attribute_taxonomy_slug' => 'pa_color',
'attribute_term_ids' => [],
],
false,
],
'missing taxonomy slug' =>
[
[
'operator' => 'any',
'attribute_type' => 'taxonomy',
'attribute_taxonomy_slug' => '',
'attribute_term_ids' => ['1'],
],
false,
],
'valid taxonomy' => [
[
'operator' => 'any',
'attribute_type' => 'taxonomy',
'attribute_taxonomy_slug' => 'pa_something',
'attribute_term_ids' => ['1'],
],
true,
],
'missing operator' =>
[
[
'operator' => '',
'attribute_type' => 'taxonomy',
'attribute_taxonomy_slug' => 'pa_color',
'attribute_term_ids' => ['1'],
],
false,
],
'invalid operator' =>
[
[
'operator' => 'anyyyyy',
'attribute_type' => 'taxonomy',
'attribute_taxonomy_slug' => 'pa_color',
'attribute_term_ids' => ['1'],
],
false,
],
'missing name' =>
[
[
'operator' => 'any',
'attribute_type' => 'local',
'attribute_local_name' => '',
'attribute_local_values' => ['1'],
],
false,
],
'missing values' =>
[
[
'operator' => 'any',
'attribute_type' => 'local',
'attribute_local_name' => 'color',
'attribute_local_values' => [],
],
false,
],
'valid local' =>
[
[
'operator' => 'any',
'attribute_type' => 'local',
'attribute_local_name' => 'color',
'attribute_local_values' => ['red'],
],
true,
],
];
}
private function assertFilterReturnsEmails(string $operator, string $attributeTaxonomySlug, array $termIds, array $expectedEmails): void {
private function assertFilterReturnsEmailsForTaxonomyAttributes(string $operator, string $attributeTaxonomySlug, array $termIds, array $expectedEmails): void {
$filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommercePurchasedWithAttribute::ACTION, [
'operator' => $operator,
'attribute_taxonomy_slug' => $attributeTaxonomySlug,
'attribute_term_ids' => $termIds,
'attribute_type' => 'taxonomy',
]);
$emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($filterData, $this->filter);
$this->assertEqualsCanonicalizing($expectedEmails, $emails);
}
private function assertFilterReturnsEmailsForLocalAttributes(string $operator, string $localAttributeName, array $localAttributeValues, array $expectedEmails): void {
$filterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_WOOCOMMERCE, WooCommercePurchasedWithAttribute::ACTION, [
'operator' => $operator,
'attribute_local_name' => $localAttributeName,
'attribute_local_values' => $localAttributeValues,
'attribute_type' => 'local',
]);
$emails = $this->tester->getSubscriberEmailsMatchingDynamicFilter($filterData, $this->filter);
$this->assertEqualsCanonicalizing($expectedEmails, $emails);

View File

@@ -10,6 +10,7 @@
var wordpress_editable_roles_list = <%= json_encode(wordpress_editable_roles_list) %>;
var mailpoet_newsletters_list = <%= json_encode(newsletters_list) %>;
var mailpoet_product_attributes = <%= json_encode(product_attributes) %>;
var mailpoet_local_product_attributes = <%= json_encode(local_product_attributes) %>;
var mailpoet_product_categories = <%= json_encode(product_categories) %>;
var mailpoet_products = <%= json_encode(products) %>;
var mailpoet_membership_plans = <%= json_encode(membership_plans) %>;