| 1 |
<?php namespace TierPricingTable\Addons\Tools\PricingTest; |
| 2 |
|
| 3 |
use TierPricingTable\Addons\GlobalTieredPricing\GlobalPricingRule; |
| 4 |
use WC_Product; |
| 5 |
use WP_User; |
| 6 |
|
| 7 |
/** |
| 8 |
* Explains why a global pricing rule matches, or does not match, a product and a customer. |
| 9 |
* Pure: reads the rule, the product and the user and returns sentences; PricingTester runs the pricing. |
| 10 |
*/ |
| 11 |
class RuleTester { |
| 12 |
|
| 13 |
/** |
| 14 |
* Why a rule matches, or does not match, a product and a customer. Mirrors |
| 15 |
* GlobalPricingRule::matchRequirements() step by step; the verdict itself comes from that method. |
| 16 |
* |
| 17 |
* @return array{matches: bool, reasons: string[]} |
| 18 |
*/ |
| 19 |
public function explain( GlobalPricingRule $rule, WP_User $user, WC_Product $product ): array { |
| 20 |
$parent = $product->is_type( array( 'variation', 'subscription-variation' ) ) ? wc_get_product( $product->get_parent_id() ) : $product; |
| 21 |
$parent = $parent ? $parent : $product; |
| 22 |
|
| 23 |
$productIds = array( (int) $product->get_id(), (int) $parent->get_id() ); |
| 24 |
$categories = array_map( 'intval', (array) $parent->get_category_ids() ); |
| 25 |
$tags = array_map( 'intval', (array) $parent->get_tag_ids() ); |
| 26 |
$brands = wp_get_post_terms( $parent->get_id(), 'product_brand', array( 'fields' => 'ids' ) ); |
| 27 |
$brands = is_wp_error( $brands ) ? array() : array_map( 'intval', (array) $brands ); |
| 28 |
$roles = GlobalPricingRule::rolesOf( $user ); // a visitor has the guest pseudo role |
| 29 |
|
| 30 |
$reasons = array(); |
| 31 |
|
| 32 |
// 1. product exclusions |
| 33 |
if ( array_intersect( $productIds, $rule->getExcludedProducts() ) ) { |
| 34 |
$reasons[] = __( 'The product is listed under the excluded products.', 'tier-pricing-table' ); |
| 35 |
} |
| 36 |
foreach ( array( |
| 37 |
array( $categories, $rule->getEffectiveExcludedCategories(), 'product_cat', __( 'The product is in an excluded category: %s.', 'tier-pricing-table' ) ), |
| 38 |
array( $tags, $rule->getExcludedProductTags(), 'product_tag', __( 'The product has an excluded tag: %s.', 'tier-pricing-table' ) ), |
| 39 |
array( $brands, $rule->getExcludedProductBrands(), 'product_brand', __( 'The product has an excluded brand: %s.', 'tier-pricing-table' ) ), |
| 40 |
) as list( $productTerms, $excluded, $taxonomy, $message ) ) { |
| 41 |
$hit = array_intersect( $productTerms, array_map( 'intval', $excluded ) ); |
| 42 |
if ( $hit ) { |
| 43 |
$reasons[] = sprintf( $message, $this->termNames( $hit, $taxonomy ) ); |
| 44 |
} |
| 45 |
} |
| 46 |
|
| 47 |
// 2. customer exclusions |
| 48 |
if ( $user->ID && in_array( $user->ID, array_map( 'intval', $rule->getExcludedUsers() ), true ) ) { |
| 49 |
$reasons[] = __( 'The customer is listed under the excluded customers.', 'tier-pricing-table' ); |
| 50 |
} |
| 51 |
$excludedRoles = array_intersect( $roles, $rule->getExcludedUserRoles() ); |
| 52 |
if ( $excludedRoles ) { |
| 53 |
$reasons[] = sprintf( __( 'The customer has an excluded role: %s.', 'tier-pricing-table' ), $this->roleNames( $excludedRoles ) ); |
| 54 |
} |
| 55 |
|
| 56 |
if ( $reasons ) { |
| 57 |
return array( 'matches' => false, 'reasons' => $reasons ); |
| 58 |
} |
| 59 |
|
| 60 |
// 3. product limitations |
| 61 |
$limited = false; |
| 62 |
$matched = array(); |
| 63 |
$checks = array( |
| 64 |
array( $productIds, $rule->getIncludedProducts(), null, __( 'The product is listed under the rule\'s products.', 'tier-pricing-table' ) ), |
| 65 |
array( $categories, $rule->getEffectiveIncludedCategories(), 'product_cat', __( 'The product is in the rule\'s category: %s.', 'tier-pricing-table' ) ), |
| 66 |
array( $tags, $rule->getIncludedProductTags(), 'product_tag', __( 'The product has the rule\'s tag: %s.', 'tier-pricing-table' ) ), |
| 67 |
array( $brands, $rule->getIncludedProductBrands(), 'product_brand', __( 'The product has the rule\'s brand: %s.', 'tier-pricing-table' ) ), |
| 68 |
); |
| 69 |
foreach ( $checks as list( $productTerms, $included, $taxonomy, $message ) ) { |
| 70 |
if ( empty( $included ) ) { |
| 71 |
continue; |
| 72 |
} |
| 73 |
$limited = true; |
| 74 |
$hit = array_intersect( $productTerms, array_map( 'intval', $included ) ); |
| 75 |
if ( $hit ) { |
| 76 |
$matched[] = $taxonomy ? sprintf( $message, $this->termNames( $hit, $taxonomy ) ) : $message; |
| 77 |
} |
| 78 |
} |
| 79 |
|
| 80 |
if ( $limited && ! $matched ) { |
| 81 |
$scope = array(); |
| 82 |
if ( $rule->getIncludedProductCategories() ) { |
| 83 |
$scope[] = sprintf( |
| 84 |
$rule->isIncludeSubcategories() |
| 85 |
/* translators: %s: category names */ |
| 86 |
? __( 'categories %s and their subcategories', 'tier-pricing-table' ) |
| 87 |
/* translators: %s: category names */ |
| 88 |
: __( 'categories %s', 'tier-pricing-table' ), |
| 89 |
$this->termNames( $rule->getIncludedProductCategories(), 'product_cat' ) |
| 90 |
); |
| 91 |
} |
| 92 |
if ( $rule->getIncludedProductTags() ) { |
| 93 |
$scope[] = sprintf( __( 'tags %s', 'tier-pricing-table' ), $this->termNames( $rule->getIncludedProductTags(), 'product_tag' ) ); |
| 94 |
} |
| 95 |
if ( $rule->getIncludedProductBrands() ) { |
| 96 |
$scope[] = sprintf( __( 'brands %s', 'tier-pricing-table' ), $this->termNames( $rule->getIncludedProductBrands(), 'product_brand' ) ); |
| 97 |
} |
| 98 |
if ( $rule->getIncludedProducts() ) { |
| 99 |
$scope[] = sprintf( _n( '%d selected product', '%d selected products', count( $rule->getIncludedProducts() ), 'tier-pricing-table' ), count( $rule->getIncludedProducts() ) ); |
| 100 |
} |
| 101 |
$own = $categories ? sprintf( __( 'The product is in %s.', 'tier-pricing-table' ), $this->termNames( $categories, 'product_cat' ) ) : __( 'The product has no category.', 'tier-pricing-table' ); |
| 102 |
|
| 103 |
return array( |
| 104 |
'matches' => false, |
| 105 |
'reasons' => array( sprintf( __( 'The rule is limited to %s, and the product is in none of them.', 'tier-pricing-table' ), implode( '; ', $scope ) ), $own ), |
| 106 |
); |
| 107 |
} |
| 108 |
|
| 109 |
$reasons = $matched ? $matched : array( __( 'The rule applies to every product (no products, categories, tags or brands selected).', 'tier-pricing-table' ) ); |
| 110 |
|
| 111 |
// 4. and 5. customer limitations |
| 112 |
$includedRoles = $rule->getIncludedUserRoles(); |
| 113 |
$includedUsers = array_map( 'intval', $rule->getIncludedUsers() ); |
| 114 |
|
| 115 |
if ( empty( $includedRoles ) && empty( $includedUsers ) ) { |
| 116 |
$reasons[] = __( 'The rule applies to every customer (no roles or customers selected).', 'tier-pricing-table' ); |
| 117 |
|
| 118 |
return array( 'matches' => true, 'reasons' => $reasons ); |
| 119 |
} |
| 120 |
|
| 121 |
if ( $user->ID && in_array( $user->ID, $includedUsers, true ) ) { |
| 122 |
$reasons[] = __( 'The customer is listed under the rule\'s customers.', 'tier-pricing-table' ); |
| 123 |
|
| 124 |
return array( 'matches' => true, 'reasons' => $reasons ); |
| 125 |
} |
| 126 |
|
| 127 |
$roleHit = array_intersect( $roles, $includedRoles ); |
| 128 |
if ( $roleHit ) { |
| 129 |
$reasons[] = sprintf( __( 'The customer has the rule\'s role: %s.', 'tier-pricing-table' ), $this->roleNames( $roleHit ) ); |
| 130 |
|
| 131 |
return array( 'matches' => true, 'reasons' => $reasons ); |
| 132 |
} |
| 133 |
|
| 134 |
$who = $user->ID |
| 135 |
? ( $user->roles ? sprintf( __( 'This customer has the roles %s.', 'tier-pricing-table' ), $this->roleNames( $user->roles ) ) : __( 'This customer has no role.', 'tier-pricing-table' ) ) |
| 136 |
: __( 'A visitor who is not logged in only matches rules that include "Guests (not logged in)" or have no customer limits.', 'tier-pricing-table' ); |
| 137 |
$limits = array(); |
| 138 |
if ( $includedRoles ) { |
| 139 |
$limits[] = sprintf( __( 'roles %s', 'tier-pricing-table' ), $this->roleNames( $includedRoles ) ); |
| 140 |
} |
| 141 |
if ( $includedUsers ) { |
| 142 |
$limits[] = sprintf( _n( '%d selected customer', '%d selected customers', count( $includedUsers ), 'tier-pricing-table' ), count( $includedUsers ) ); |
| 143 |
} |
| 144 |
|
| 145 |
return array( |
| 146 |
'matches' => false, |
| 147 |
'reasons' => array( sprintf( __( 'The rule is limited to %s.', 'tier-pricing-table' ), implode( ' ' . __( 'and', 'tier-pricing-table' ) . ' ', $limits ) ), $who ), |
| 148 |
); |
| 149 |
} |
| 150 |
|
| 151 |
protected function termNames( array $ids, string $taxonomy ): string { |
| 152 |
$names = array(); |
| 153 |
foreach ( $ids as $id ) { |
| 154 |
$term = get_term( (int) $id, $taxonomy ); |
| 155 |
$names[] = $term && ! is_wp_error( $term ) ? $term->name : '#' . (int) $id; |
| 156 |
} |
| 157 |
|
| 158 |
return implode( ', ', $names ); |
| 159 |
} |
| 160 |
|
| 161 |
public function roleNames( array $roles ): string { |
| 162 |
$names = array(); |
| 163 |
$all = function_exists( 'wp_roles' ) ? wp_roles()->role_names : array(); |
| 164 |
foreach ( $roles as $role ) { |
| 165 |
if ( GlobalPricingRule::GUEST_ROLE === $role ) { |
| 166 |
$names[] = __( 'Guests (not logged in)', 'tier-pricing-table' ); |
| 167 |
continue; |
| 168 |
} |
| 169 |
$names[] = isset( $all[ $role ] ) ? translate_user_role( $all[ $role ] ) : $role; |
| 170 |
} |
| 171 |
|
| 172 |
return implode( ', ', $names ); |
| 173 |
} |
| 174 |
|
| 175 |
/** |
| 176 |
* Human label of the provider that produced the pricing. |
| 177 |
*/ |
| 178 |
public static function providerLabel( string $provider ): string { |
| 179 |
switch ( $provider ) { |
| 180 |
case 'role-based': |
| 181 |
return __( 'a role price on the product', 'tier-pricing-table' ); |
| 182 |
case 'user-based': |
| 183 |
return __( 'a customer price on the product', 'tier-pricing-table' ); |
| 184 |
case 'global-rules': |
| 185 |
return __( 'a global pricing rule', 'tier-pricing-table' ); |
| 186 |
case 'product': |
| 187 |
return __( 'the product\'s own pricing', 'tier-pricing-table' ); |
| 188 |
default: |
| 189 |
return $provider ? $provider : __( 'the product\'s own pricing', 'tier-pricing-table' ); |
| 190 |
} |
| 191 |
} |
| 192 |
} |
| 193 |
|