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:
committed by
Aschepikov
parent
24e38d6752
commit
a4a2ef3b1f
@@ -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,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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) %>;
|
||||
|
Reference in New Issue
Block a user