| 1 |
<?php |
| 2 |
|
| 3 |
namespace TierPricingTable\Addons\GlobalTieredPricing; |
| 4 |
|
| 5 |
use TierPricingTable\Addons\GlobalTieredPricing\PricingRule\RuleSettings; |
| 6 |
use TierPricingTable\Forms\Form; |
| 7 |
use DateTime; |
| 8 |
use DateTimeZone; |
| 9 |
use Exception; |
| 10 |
use WC_Product; |
| 11 |
use WP_User; |
| 12 |
class GlobalPricingRule { |
| 13 |
/** |
| 14 |
* Rule ID |
| 15 |
* |
| 16 |
* @var int |
| 17 |
*/ |
| 18 |
public $id; |
| 19 |
|
| 20 |
/** |
| 21 |
* Is suspended |
| 22 |
* |
| 23 |
* @var bool |
| 24 |
*/ |
| 25 |
public $isSuspended = false; |
| 26 |
|
| 27 |
/** |
| 28 |
* Regular pricing type |
| 29 |
* |
| 30 |
* @var string |
| 31 |
*/ |
| 32 |
public $pricingType; |
| 33 |
|
| 34 |
/** |
| 35 |
* Regular price |
| 36 |
* |
| 37 |
* @var ?float |
| 38 |
*/ |
| 39 |
public $regularPrice; |
| 40 |
|
| 41 |
/** |
| 42 |
* Sale price |
| 43 |
* |
| 44 |
* @var ?float |
| 45 |
*/ |
| 46 |
public $salePrice; |
| 47 |
|
| 48 |
/** |
| 49 |
* Percentage Discount |
| 50 |
* |
| 51 |
* @var ?float |
| 52 |
*/ |
| 53 |
public $discount; |
| 54 |
|
| 55 |
/** |
| 56 |
* Percentage Discount |
| 57 |
* |
| 58 |
* @var string |
| 59 |
*/ |
| 60 |
public $discountType = 'sale_price'; |
| 61 |
|
| 62 |
/** |
| 63 |
* Applying type |
| 64 |
* |
| 65 |
* @var string |
| 66 |
*/ |
| 67 |
public $applyingType; |
| 68 |
|
| 69 |
/** |
| 70 |
* Tax status |
| 71 |
* |
| 72 |
* @var string |
| 73 |
*/ |
| 74 |
public $taxStatus = ''; |
| 75 |
|
| 76 |
/** |
| 77 |
* Tax class |
| 78 |
* |
| 79 |
* @var string |
| 80 |
*/ |
| 81 |
public $taxClass = ''; |
| 82 |
|
| 83 |
/** |
| 84 |
* Tiered Pricing type |
| 85 |
* |
| 86 |
* @var string |
| 87 |
*/ |
| 88 |
public $tieredPricingType; |
| 89 |
|
| 90 |
/** |
| 91 |
* Percentage Tiered Pricing Rules |
| 92 |
* |
| 93 |
* @var array |
| 94 |
*/ |
| 95 |
public $percentageTieredPricingRules = array(); |
| 96 |
|
| 97 |
/** |
| 98 |
* Fixed Tiered Pricing Rules |
| 99 |
* |
| 100 |
* @var array |
| 101 |
*/ |
| 102 |
public $fixedTieredPricingRules = array(); |
| 103 |
|
| 104 |
/** |
| 105 |
* Included categories |
| 106 |
* |
| 107 |
* @var array |
| 108 |
*/ |
| 109 |
public $includedProductCategories = array(); |
| 110 |
|
| 111 |
/** |
| 112 |
* Excluded categories |
| 113 |
* |
| 114 |
* @var array |
| 115 |
*/ |
| 116 |
public $excludedProductCategories = array(); |
| 117 |
|
| 118 |
/** |
| 119 |
* Whether the included and excluded categories cover their subcategories too. |
| 120 |
* |
| 121 |
* @var bool |
| 122 |
*/ |
| 123 |
public $includeSubcategories = false; |
| 124 |
|
| 125 |
/** |
| 126 |
* Included tags |
| 127 |
* |
| 128 |
* @var array |
| 129 |
*/ |
| 130 |
public $includedProductTags = array(); |
| 131 |
|
| 132 |
/** |
| 133 |
* Excluded tags |
| 134 |
* |
| 135 |
* @var array |
| 136 |
*/ |
| 137 |
public $excludedProductTags = array(); |
| 138 |
|
| 139 |
/** |
| 140 |
* Included brands |
| 141 |
* |
| 142 |
* @var array |
| 143 |
*/ |
| 144 |
public $includedProductBrands = array(); |
| 145 |
|
| 146 |
/** |
| 147 |
* Excluded brands |
| 148 |
* |
| 149 |
* @var array |
| 150 |
*/ |
| 151 |
public $excludedProductBrands = array(); |
| 152 |
|
| 153 |
/** |
| 154 |
* Included products |
| 155 |
* |
| 156 |
* @var array |
| 157 |
*/ |
| 158 |
public $includedProducts = array(); |
| 159 |
|
| 160 |
/** |
| 161 |
* Excluded products |
| 162 |
* |
| 163 |
* @var array |
| 164 |
*/ |
| 165 |
public $excludedProducts = array(); |
| 166 |
|
| 167 |
/** |
| 168 |
* Included product roles |
| 169 |
* |
| 170 |
* @var array |
| 171 |
*/ |
| 172 |
public $includedUsersRole = array(); |
| 173 |
|
| 174 |
/** |
| 175 |
* Excluded product roles |
| 176 |
* |
| 177 |
* @var array |
| 178 |
*/ |
| 179 |
public $excludedUsersRole = array(); |
| 180 |
|
| 181 |
/** |
| 182 |
* Included users |
| 183 |
* |
| 184 |
* @var array |
| 185 |
*/ |
| 186 |
public $includedUsers = array(); |
| 187 |
|
| 188 |
/** |
| 189 |
* Excluded users |
| 190 |
* |
| 191 |
* @var array |
| 192 |
*/ |
| 193 |
public $excludedUsers = array(); |
| 194 |
|
| 195 |
/** |
| 196 |
* Product minimum purchase quantity |
| 197 |
* |
| 198 |
* @var int|null |
| 199 |
*/ |
| 200 |
public $minimum; |
| 201 |
|
| 202 |
/** |
| 203 |
* Mix and match minimum quantity |
| 204 |
* |
| 205 |
* @var bool|null |
| 206 |
*/ |
| 207 |
public $mixAndMatchMinQuantity = null; |
| 208 |
|
| 209 |
public $priorityOptions; |
| 210 |
|
| 211 |
const DEFAULT_PRIORITY = 10; |
| 212 |
|
| 213 |
/** |
| 214 |
* The pseudo role of a visitor who is not logged in, selectable in the role lists. The same key the |
| 215 |
* wholesale registration add-on uses (NonLoggedInUsers\Roles::GUEST). |
| 216 |
*/ |
| 217 |
const GUEST_ROLE = 'guest'; |
| 218 |
|
| 219 |
/** |
| 220 |
* The roles the matcher reads for a user: a visitor who is not logged in has the guest pseudo role. |
| 221 |
*/ |
| 222 |
public static function rolesOf( WP_User $user ) : array { |
| 223 |
return ( $user->ID ? (array) $user->roles : array(self::GUEST_ROLE) ); |
| 224 |
} |
| 225 |
|
| 226 |
/** |
| 227 |
* Whether a role key may be stored in the rule's role lists: an existing WordPress role or the guest pseudo role. |
| 228 |
*/ |
| 229 |
public static function isSelectableRole( $role ) : bool { |
| 230 |
return self::GUEST_ROLE === $role || function_exists( 'wp_roles' ) && array_key_exists( $role, wp_roles()->roles ); |
| 231 |
} |
| 232 |
|
| 233 |
/** |
| 234 |
* Order among global rules: the lowest number is applied first; equal numbers, the newest rule first. |
| 235 |
* |
| 236 |
* @var int |
| 237 |
*/ |
| 238 |
public $priority = self::DEFAULT_PRIORITY; |
| 239 |
|
| 240 |
/** |
| 241 |
* First day the rule applies (Y-m-d in the site's timezone, inclusive), null = no start. |
| 242 |
* |
| 243 |
* @var ?string |
| 244 |
*/ |
| 245 |
public $startDate = null; |
| 246 |
|
| 247 |
/** |
| 248 |
* Last day the rule applies (Y-m-d in the site's timezone, inclusive), null = no end. |
| 249 |
* |
| 250 |
* @var ?string |
| 251 |
*/ |
| 252 |
public $endDate = null; |
| 253 |
|
| 254 |
/** |
| 255 |
* post_date_gmt of the rule: orders rules with the same priority (newest first). |
| 256 |
* |
| 257 |
* @var string |
| 258 |
*/ |
| 259 |
public $createdAt = ''; |
| 260 |
|
| 261 |
/** |
| 262 |
* Array with custom data from 3rd-party addons |
| 263 |
* |
| 264 |
* @var array |
| 265 |
*/ |
| 266 |
public $data = array(); |
| 267 |
|
| 268 |
public function getId() : int { |
| 269 |
return $this->id; |
| 270 |
} |
| 271 |
|
| 272 |
public function setId( int $id ) { |
| 273 |
$this->id = $id; |
| 274 |
} |
| 275 |
|
| 276 |
public function getDiscount() : ?float { |
| 277 |
return $this->discount; |
| 278 |
} |
| 279 |
|
| 280 |
public function setDiscount( ?float $discount ) { |
| 281 |
$this->discount = $discount; |
| 282 |
} |
| 283 |
|
| 284 |
public function getDiscountType() : string { |
| 285 |
return $this->discountType; |
| 286 |
} |
| 287 |
|
| 288 |
public function setDiscountType( string $discountType ) { |
| 289 |
$this->discountType = ( in_array( $discountType, array('sale_price', 'regular_price') ) ? $discountType : 'sale_price' ); |
| 290 |
} |
| 291 |
|
| 292 |
public function getTieredPricingType() : string { |
| 293 |
return 'fixed'; |
| 294 |
} |
| 295 |
|
| 296 |
public function setTieredPricingType( string $tieredPricingType ) { |
| 297 |
$this->tieredPricingType = ( in_array( $tieredPricingType, array('percentage', 'fixed') ) ? $tieredPricingType : 'fixed' ); |
| 298 |
} |
| 299 |
|
| 300 |
public function getPercentageTieredPricingRules() : array { |
| 301 |
return $this->percentageTieredPricingRules; |
| 302 |
} |
| 303 |
|
| 304 |
public function setPercentageTieredPricingRules( array $percentageTieredPricingRules ) { |
| 305 |
$this->percentageTieredPricingRules = $percentageTieredPricingRules; |
| 306 |
} |
| 307 |
|
| 308 |
public function getFixedTieredPricingRules() : array { |
| 309 |
return $this->fixedTieredPricingRules; |
| 310 |
} |
| 311 |
|
| 312 |
public function setFixedTieredPricingRules( array $fixedTieredPricingRules ) { |
| 313 |
$this->fixedTieredPricingRules = $fixedTieredPricingRules; |
| 314 |
} |
| 315 |
|
| 316 |
public function getApplyingType() : string { |
| 317 |
return $this->applyingType; |
| 318 |
} |
| 319 |
|
| 320 |
public function setApplyingType( string $applyingType ) { |
| 321 |
$this->applyingType = ( in_array( $applyingType, array('individual', 'cross') ) ? $applyingType : 'cross' ); |
| 322 |
} |
| 323 |
|
| 324 |
public function getPricingType() : string { |
| 325 |
return $this->pricingType; |
| 326 |
} |
| 327 |
|
| 328 |
public function setPricingType( string $priceType ) { |
| 329 |
$this->pricingType = ( in_array( $priceType, array('percentage', 'flat') ) ? $priceType : 'flat' ); |
| 330 |
} |
| 331 |
|
| 332 |
public function getTieredPricingRules() : array { |
| 333 |
return $this->getFixedTieredPricingRules(); |
| 334 |
} |
| 335 |
|
| 336 |
public function getTaxStatus() : string { |
| 337 |
return $this->taxStatus; |
| 338 |
} |
| 339 |
|
| 340 |
public function setTaxStatus( string $taxStatus ) { |
| 341 |
$this->taxStatus = $taxStatus; |
| 342 |
} |
| 343 |
|
| 344 |
public function getTaxClass() : string { |
| 345 |
return $this->taxClass; |
| 346 |
} |
| 347 |
|
| 348 |
public function setTaxClass( string $taxClass ) { |
| 349 |
$this->taxClass = $taxClass; |
| 350 |
} |
| 351 |
|
| 352 |
public function getRegularPrice() : ?float { |
| 353 |
return $this->regularPrice; |
| 354 |
} |
| 355 |
|
| 356 |
public function setRegularPrice( ?float $regularPrice ) { |
| 357 |
$this->regularPrice = ( !Form::isEmpty( $regularPrice ) ? floatval( $regularPrice ) : null ); |
| 358 |
} |
| 359 |
|
| 360 |
public function getSalePrice() : ?float { |
| 361 |
return $this->salePrice; |
| 362 |
} |
| 363 |
|
| 364 |
public function setSalePrice( ?float $salePrice ) { |
| 365 |
$this->salePrice = $salePrice; |
| 366 |
} |
| 367 |
|
| 368 |
/** |
| 369 |
* Create instance from array |
| 370 |
* |
| 371 |
* @param array $data |
| 372 |
* |
| 373 |
* @return self |
| 374 |
*/ |
| 375 |
public static function fromArray( array $data ) : self { |
| 376 |
$applyingType = $data['applying_type'] ?? 'individual'; |
| 377 |
$applyingType = ( in_array( $applyingType, array('individual', 'cross') ) ? $applyingType : 'cross' ); |
| 378 |
$tieredPricingType = $data['tiered_pricing_type'] ?? 'fixed'; |
| 379 |
$tieredPricingType = ( in_array( $tieredPricingType, array('flat', 'percentage') ) ? $tieredPricingType : 'fixed' ); |
| 380 |
$percentageRules = ( isset( $data['percentage_rules'] ) ? (array) $data['percentage_rules'] : array() ); |
| 381 |
$fixedRules = ( isset( $data['fixed_rules'] ) ? (array) $data['fixed_rules'] : array() ); |
| 382 |
$pricingType = $data['pricing_type'] ?? 'flat'; |
| 383 |
$pricingType = ( in_array( $pricingType, array('flat', 'percentage') ) ? $pricingType : 'flat' ); |
| 384 |
$regularPrice = $data['regular_price'] ?? null; |
| 385 |
$salePrice = $data['sale_price'] ?? null; |
| 386 |
$discount = $data['discount'] ?? null; |
| 387 |
$discountType = $data['discount_type'] ?? 'sale_price'; |
| 388 |
$minimum = $data['minimum'] ?? null; |
| 389 |
$mixAndMatchValue = $data['mix_and_match_minimum'] ?? ''; |
| 390 |
$mixAndMatch = ( $mixAndMatchValue === '' ? null : $mixAndMatchValue === 'yes' ); |
| 391 |
$taxStatus = $data['tax_status'] ?? ''; |
| 392 |
$taxClass = $data['tax_class'] ?? ''; |
| 393 |
$self = new self(); |
| 394 |
$self->setTaxStatus( $taxStatus ); |
| 395 |
$self->setTaxClass( $taxClass ); |
| 396 |
$self->setPricingType( (string) $pricingType ); |
| 397 |
$self->setRegularPrice( ( Form::isEmpty( $regularPrice ) ? null : (float) $regularPrice ) ); |
| 398 |
$self->setSalePrice( ( Form::isEmpty( $salePrice ) ? null : (float) $salePrice ) ); |
| 399 |
$self->setDiscount( ( Form::isEmpty( $discount ) ? null : (float) $discount ) ); |
| 400 |
$self->setDiscountType( (string) $discountType ); |
| 401 |
$self->setApplyingType( $applyingType ); |
| 402 |
$self->setTieredPricingType( $tieredPricingType ); |
| 403 |
$self->setPercentageTieredPricingRules( $percentageRules ); |
| 404 |
$self->setFixedTieredPricingRules( $fixedRules ); |
| 405 |
$self->setMinimum( ( Form::isEmpty( $minimum ) ? null : (int) $minimum ) ); |
| 406 |
$self->setMixAndMatchMinQuantity( $mixAndMatch ); |
| 407 |
$self->setPriority( $data['priority'] ?? self::DEFAULT_PRIORITY ); |
| 408 |
$self->setStartDate( $data['start_date'] ?? null ); |
| 409 |
$self->setEndDate( $data['end_date'] ?? null ); |
| 410 |
return $self; |
| 411 |
} |
| 412 |
|
| 413 |
/** |
| 414 |
* Validate |
| 415 |
* |
| 416 |
* @throws Exception |
| 417 |
*/ |
| 418 |
public function validatePricing() { |
| 419 |
// only the fields of the chosen pricing type reach the storefront: a discount left over from |
| 420 |
// "percentage" changes nothing in "flat" mode, and the other way round |
| 421 |
if ( 'percentage' === $this->getPricingType() ) { |
| 422 |
$valid = (float) $this->getDiscount() > 0; |
| 423 |
} else { |
| 424 |
$valid = !Form::isEmpty( $this->getRegularPrice() ) || !Form::isEmpty( $this->getSalePrice() ); |
| 425 |
} |
| 426 |
$valid = $valid || !empty( $this->getTieredPricingRules() ); |
| 427 |
$valid = $valid || !Form::isEmpty( $this->getMinimum() ); |
| 428 |
// a mix & match choice alone changes how product-level minimums count across variations |
| 429 |
$valid = $valid || !is_null( $this->getMixAndMatchMinQuantity() ); |
| 430 |
$valid = $valid || $this->getSettings()->getPriorityType() === 'flexible'; |
| 431 |
$valid = apply_filters( 'tiered_pricing_table/global_pricing/validation', $valid, $this ); |
| 432 |
if ( !$valid ) { |
| 433 |
throw new Exception(esc_html__( 'The pricing rule does not affect either prices or product quantity. The rule will be skipped.', 'tier-pricing-table' )); |
| 434 |
} |
| 435 |
} |
| 436 |
|
| 437 |
public function isValidPricing() : bool { |
| 438 |
try { |
| 439 |
$this->validatePricing(); |
| 440 |
} catch ( Exception $e ) { |
| 441 |
return false; |
| 442 |
} |
| 443 |
return true; |
| 444 |
} |
| 445 |
|
| 446 |
public function getMinimum() : ?int { |
| 447 |
return $this->minimum; |
| 448 |
} |
| 449 |
|
| 450 |
public function setMinimum( ?int $minimum ) { |
| 451 |
$this->minimum = ( intval( $minimum ) > 1 ? $minimum : null ); |
| 452 |
} |
| 453 |
|
| 454 |
public function getMixAndMatchMinQuantity() : ?bool { |
| 455 |
return $this->mixAndMatchMinQuantity; |
| 456 |
} |
| 457 |
|
| 458 |
public function setMixAndMatchMinQuantity( ?bool $mixAndMatch ) { |
| 459 |
$this->mixAndMatchMinQuantity = $mixAndMatch; |
| 460 |
} |
| 461 |
|
| 462 |
public function getIncludedProductCategories() : array { |
| 463 |
return $this->includedProductCategories; |
| 464 |
} |
| 465 |
|
| 466 |
public function getExcludedProductCategories() : array { |
| 467 |
return $this->excludedProductCategories; |
| 468 |
} |
| 469 |
|
| 470 |
public function setIncludedProductCategories( array $includedProductCategories ) { |
| 471 |
$this->includedProductCategories = $includedProductCategories; |
| 472 |
} |
| 473 |
|
| 474 |
public function setExcludedProductCategories( array $excludedProductCategories ) { |
| 475 |
$this->excludedProductCategories = $excludedProductCategories; |
| 476 |
} |
| 477 |
|
| 478 |
public function isIncludeSubcategories() : bool { |
| 479 |
return $this->includeSubcategories; |
| 480 |
} |
| 481 |
|
| 482 |
public function setIncludeSubcategories( bool $includeSubcategories ) { |
| 483 |
$this->includeSubcategories = $includeSubcategories; |
| 484 |
} |
| 485 |
|
| 486 |
/** |
| 487 |
* The included categories as the matcher reads them: with every subcategory when the switch is on. |
| 488 |
*/ |
| 489 |
public function getEffectiveIncludedCategories() : array { |
| 490 |
return ( $this->includeSubcategories ? self::withSubcategories( $this->includedProductCategories ) : $this->includedProductCategories ); |
| 491 |
} |
| 492 |
|
| 493 |
/** |
| 494 |
* The excluded categories as the matcher reads them: with every subcategory when the switch is on. |
| 495 |
*/ |
| 496 |
public function getEffectiveExcludedCategories() : array { |
| 497 |
return ( $this->includeSubcategories ? self::withSubcategories( $this->excludedProductCategories ) : $this->excludedProductCategories ); |
| 498 |
} |
| 499 |
|
| 500 |
/** |
| 501 |
* The ids plus all their descendant product categories. |
| 502 |
*/ |
| 503 |
public static function withSubcategories( array $ids ) : array { |
| 504 |
$all = array(); |
| 505 |
foreach ( array_map( 'intval', $ids ) as $id ) { |
| 506 |
$all[] = $id; |
| 507 |
$children = get_term_children( $id, 'product_cat' ); |
| 508 |
if ( is_array( $children ) ) { |
| 509 |
$all = array_merge( $all, array_map( 'intval', $children ) ); |
| 510 |
} |
| 511 |
} |
| 512 |
return array_values( array_unique( $all ) ); |
| 513 |
} |
| 514 |
|
| 515 |
public function getIncludedProductTags() : array { |
| 516 |
return $this->includedProductTags; |
| 517 |
} |
| 518 |
|
| 519 |
public function getExcludedProductTags() : array { |
| 520 |
return $this->excludedProductTags; |
| 521 |
} |
| 522 |
|
| 523 |
public function setIncludedProductTags( array $includedProductTags ) { |
| 524 |
$this->includedProductTags = $includedProductTags; |
| 525 |
} |
| 526 |
|
| 527 |
public function setExcludedProductTags( array $excludedProductTags ) { |
| 528 |
$this->excludedProductTags = $excludedProductTags; |
| 529 |
} |
| 530 |
|
| 531 |
public function getIncludedProductBrands() : array { |
| 532 |
return $this->includedProductBrands; |
| 533 |
} |
| 534 |
|
| 535 |
public function getExcludedProductBrands() : array { |
| 536 |
return $this->excludedProductBrands; |
| 537 |
} |
| 538 |
|
| 539 |
public function setIncludedProductBrands( array $includedProductBrands ) { |
| 540 |
$this->includedProductBrands = $includedProductBrands; |
| 541 |
} |
| 542 |
|
| 543 |
public function setExcludedProductBrands( array $excludedProductBrands ) { |
| 544 |
$this->excludedProductBrands = $excludedProductBrands; |
| 545 |
} |
| 546 |
|
| 547 |
public function getIncludedProducts() : array { |
| 548 |
return $this->includedProducts; |
| 549 |
} |
| 550 |
|
| 551 |
public function getExcludedProducts() : array { |
| 552 |
return $this->excludedProducts; |
| 553 |
} |
| 554 |
|
| 555 |
public function setIncludedProducts( array $includedProducts ) { |
| 556 |
$this->includedProducts = $includedProducts; |
| 557 |
} |
| 558 |
|
| 559 |
public function setExcludedProducts( array $excludedProducts ) { |
| 560 |
$this->excludedProducts = $excludedProducts; |
| 561 |
} |
| 562 |
|
| 563 |
public function getIncludedUserRoles() : array { |
| 564 |
return $this->includedUsersRole; |
| 565 |
} |
| 566 |
|
| 567 |
public function getExcludedUserRoles() : array { |
| 568 |
return $this->excludedUsersRole; |
| 569 |
} |
| 570 |
|
| 571 |
public function setIncludedUsersRole( array $includedUsersRole ) { |
| 572 |
$this->includedUsersRole = $includedUsersRole; |
| 573 |
} |
| 574 |
|
| 575 |
public function setExcludedUsersRole( array $excludedUsersRole ) { |
| 576 |
$this->excludedUsersRole = $excludedUsersRole; |
| 577 |
} |
| 578 |
|
| 579 |
public function getIncludedUsers() : array { |
| 580 |
return $this->includedUsers; |
| 581 |
} |
| 582 |
|
| 583 |
public function getExcludedUsers() : array { |
| 584 |
return $this->excludedUsers; |
| 585 |
} |
| 586 |
|
| 587 |
public function setIncludedUsers( array $includedUsers ) { |
| 588 |
$this->includedUsers = $includedUsers; |
| 589 |
} |
| 590 |
|
| 591 |
public function setExcludedUsers( array $excludedUsers ) { |
| 592 |
$this->excludedUsers = $excludedUsers; |
| 593 |
} |
| 594 |
|
| 595 |
public function getPriority() : int { |
| 596 |
return $this->priority; |
| 597 |
} |
| 598 |
|
| 599 |
/** |
| 600 |
* @param mixed $priority Anything non-numeric (an empty meta row, for instance) falls back to the default. |
| 601 |
*/ |
| 602 |
public function setPriority( $priority ) { |
| 603 |
$this->priority = ( is_numeric( $priority ) ? max( 0, (int) $priority ) : self::DEFAULT_PRIORITY ); |
| 604 |
} |
| 605 |
|
| 606 |
public function getStartDate() : ?string { |
| 607 |
return $this->startDate; |
| 608 |
} |
| 609 |
|
| 610 |
public function setStartDate( $date ) { |
| 611 |
$this->startDate = self::normalizeDate( $date ); |
| 612 |
} |
| 613 |
|
| 614 |
public function getEndDate() : ?string { |
| 615 |
return $this->endDate; |
| 616 |
} |
| 617 |
|
| 618 |
public function setEndDate( $date ) { |
| 619 |
$this->endDate = self::normalizeDate( $date ); |
| 620 |
} |
| 621 |
|
| 622 |
public function hasSchedule() : bool { |
| 623 |
return null !== $this->startDate || null !== $this->endDate; |
| 624 |
} |
| 625 |
|
| 626 |
public function getCreatedAt() : string { |
| 627 |
return $this->createdAt; |
| 628 |
} |
| 629 |
|
| 630 |
public function setCreatedAt( string $createdAt ) { |
| 631 |
$this->createdAt = $createdAt; |
| 632 |
} |
| 633 |
|
| 634 |
/** |
| 635 |
* A "Y-m-d" string or null; anything else (a partial date, "0000-00-00", a timestamp) is dropped. |
| 636 |
*/ |
| 637 |
protected static function normalizeDate( $date ) : ?string { |
| 638 |
$date = ( is_string( $date ) ? trim( $date ) : '' ); |
| 639 |
if ( '' === $date ) { |
| 640 |
return null; |
| 641 |
} |
| 642 |
$parsed = DateTime::createFromFormat( '!Y-m-d', $date ); |
| 643 |
return ( $parsed && $parsed->format( 'Y-m-d' ) === $date ? $date : null ); |
| 644 |
} |
| 645 |
|
| 646 |
/** |
| 647 |
* 00:00:00 of the start date in the site's timezone, as a timestamp; null without a start date. |
| 648 |
*/ |
| 649 |
public function getStartTimestamp() : ?int { |
| 650 |
return ( null === $this->startDate ? null : self::dayBoundary( $this->startDate, false ) ); |
| 651 |
} |
| 652 |
|
| 653 |
/** |
| 654 |
* 23:59:59 of the end date in the site's timezone, as a timestamp; null without an end date. |
| 655 |
*/ |
| 656 |
public function getEndTimestamp() : ?int { |
| 657 |
return ( null === $this->endDate ? null : self::dayBoundary( $this->endDate, true ) ); |
| 658 |
} |
| 659 |
|
| 660 |
protected static function dayBoundary( string $date, bool $endOfDay ) : int { |
| 661 |
$timezone = ( function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone('UTC') ); |
| 662 |
$moment = new DateTime($date . (( $endOfDay ? ' 23:59:59' : ' 00:00:00' )), $timezone); |
| 663 |
return $moment->getTimestamp(); |
| 664 |
} |
| 665 |
|
| 666 |
/** |
| 667 |
* "active", "scheduled" (starts later) or "expired" (ended) at the given moment (default: now). |
| 668 |
*/ |
| 669 |
public function getScheduleStatus( ?int $now = null ) : string { |
| 670 |
$now = ( null === $now ? time() : $now ); |
| 671 |
$start = $this->getStartTimestamp(); |
| 672 |
$end = $this->getEndTimestamp(); |
| 673 |
if ( null !== $start && $now < $start ) { |
| 674 |
return 'scheduled'; |
| 675 |
} |
| 676 |
if ( null !== $end && $now > $end ) { |
| 677 |
return 'expired'; |
| 678 |
} |
| 679 |
return 'active'; |
| 680 |
} |
| 681 |
|
| 682 |
public function isWithinSchedule( ?int $now = null ) : bool { |
| 683 |
return 'active' === $this->getScheduleStatus( $now ); |
| 684 |
} |
| 685 |
|
| 686 |
const EFFECTIVE_STATUSES = array( |
| 687 |
'active', |
| 688 |
'scheduled', |
| 689 |
'expired', |
| 690 |
'suspended', |
| 691 |
'skipped' |
| 692 |
); |
| 693 |
|
| 694 |
/** |
| 695 |
* What the storefront makes of a published rule, in order of precedence: "suspended", "expired", |
| 696 |
* "skipped" (changes neither prices nor quantity limits), "scheduled" (starts later) or "active". |
| 697 |
*/ |
| 698 |
public function getEffectiveStatus( ?int $now = null ) : string { |
| 699 |
if ( $this->isSuspended() ) { |
| 700 |
return 'suspended'; |
| 701 |
} |
| 702 |
$schedule = $this->getScheduleStatus( $now ); |
| 703 |
if ( 'expired' === $schedule ) { |
| 704 |
return 'expired'; |
| 705 |
} |
| 706 |
// shown before "scheduled" so an empty campaign rule gets fixed before it starts |
| 707 |
if ( !$this->isValidPricing() ) { |
| 708 |
return 'skipped'; |
| 709 |
} |
| 710 |
return $schedule; |
| 711 |
} |
| 712 |
|
| 713 |
/** |
| 714 |
* Whether a higher-priority rule can take products and customers away from this one: only a rule |
| 715 |
* that is (or will be) in force. A suspended, expired or skipped rule is not applied anyway. |
| 716 |
*/ |
| 717 |
public function canBeOverridden( ?int $now = null ) : bool { |
| 718 |
return in_array( $this->getEffectiveStatus( $now ), array('active', 'scheduled'), true ); |
| 719 |
} |
| 720 |
|
| 721 |
/** |
| 722 |
* usort() callback: the order the rules are matched in. Lowest priority number first; among equal |
| 723 |
* numbers the newest rule first (today's behaviour for rules that never had a priority), then by ID. |
| 724 |
*/ |
| 725 |
public static function compareOrder( GlobalPricingRule $a, GlobalPricingRule $b ) : int { |
| 726 |
if ( $a->getPriority() !== $b->getPriority() ) { |
| 727 |
return $a->getPriority() <=> $b->getPriority(); |
| 728 |
} |
| 729 |
if ( $a->getCreatedAt() !== $b->getCreatedAt() ) { |
| 730 |
return strcmp( $b->getCreatedAt(), $a->getCreatedAt() ); |
| 731 |
} |
| 732 |
return (int) $b->id <=> (int) $a->id; |
| 733 |
} |
| 734 |
|
| 735 |
/** |
| 736 |
* Whether this rule has a higher priority than another one (and so is applied where both match). |
| 737 |
*/ |
| 738 |
public function outranks( GlobalPricingRule $other ) : bool { |
| 739 |
return self::compareOrder( $this, $other ) < 0; |
| 740 |
} |
| 741 |
|
| 742 |
/** |
| 743 |
* The rule's title, or "Rule #ID" for a rule saved without one. |
| 744 |
*/ |
| 745 |
public static function titleOf( int $id ) : string { |
| 746 |
$title = get_the_title( $id ); |
| 747 |
/* translators: %d: rule ID */ |
| 748 |
return ( '' !== trim( (string) $title ) ? $title : sprintf( __( 'Rule #%d', 'tier-pricing-table' ), $id ) ); |
| 749 |
} |
| 750 |
|
| 751 |
public function getTitle() : string { |
| 752 |
return self::titleOf( (int) $this->id ); |
| 753 |
} |
| 754 |
|
| 755 |
public function getSettings() : RuleSettings { |
| 756 |
if ( !$this->priorityOptions ) { |
| 757 |
$this->priorityOptions = new RuleSettings($this); |
| 758 |
} |
| 759 |
return $this->priorityOptions; |
| 760 |
} |
| 761 |
|
| 762 |
public function asArray() : array { |
| 763 |
return array( |
| 764 |
'pricing_type' => $this->getPricingType(), |
| 765 |
'regular_price' => $this->getRegularPrice(), |
| 766 |
'sale_price' => $this->getSalePrice(), |
| 767 |
'discount' => $this->getDiscount(), |
| 768 |
'discount_type' => $this->getDiscountType(), |
| 769 |
'applying_type' => $this->getApplyingType(), |
| 770 |
'tiered_pricing_type' => $this->getTieredPricingType(), |
| 771 |
'percentage_rules' => $this->getPercentageTieredPricingRules(), |
| 772 |
'fixed_rules' => $this->getFixedTieredPricingRules(), |
| 773 |
'minimum' => $this->getMinimum(), |
| 774 |
'mix_and_match_minimum' => $this->getMixAndMatchMinQuantity(), |
| 775 |
'tax_status' => $this->getTaxStatus(), |
| 776 |
'tax_class' => $this->getTaxClass(), |
| 777 |
'priority' => $this->getPriority(), |
| 778 |
'start_date' => $this->getStartDate(), |
| 779 |
'end_date' => $this->getEndDate(), |
| 780 |
'included_categories' => $this->getIncludedProductCategories(), |
| 781 |
'include_subcategories' => $this->isIncludeSubcategories(), |
| 782 |
'included_tags' => $this->getIncludedProductTags(), |
| 783 |
'included_brands' => $this->getIncludedProductBrands(), |
| 784 |
'included_products' => $this->getIncludedProducts(), |
| 785 |
'included_users' => $this->getIncludedUsers(), |
| 786 |
'included_users_role' => $this->getIncludedUserRoles(), |
| 787 |
'excluded_categories' => $this->getExcludedProductCategories(), |
| 788 |
'excluded_tags' => $this->getExcludedProductTags(), |
| 789 |
'excluded_brands' => $this->getExcludedProductBrands(), |
| 790 |
'excluded_products' => $this->getExcludedProducts(), |
| 791 |
'excluded_users' => $this->getExcludedUsers(), |
| 792 |
'excluded_users_role' => $this->getExcludedUserRoles(), |
| 793 |
'rule_id' => $this->getId(), |
| 794 |
'is_suspended' => $this->isSuspended(), |
| 795 |
); |
| 796 |
} |
| 797 |
|
| 798 |
public function save() { |
| 799 |
$dataToUpdate = array( |
| 800 |
'_tpt_pricing_type' => $this->getPricingType(), |
| 801 |
'_tpt_regular_price' => $this->getRegularPrice(), |
| 802 |
'_tpt_sale_price' => $this->getSalePrice(), |
| 803 |
'_tpt_discount' => $this->getDiscount(), |
| 804 |
'_tpt_discount_type' => $this->getDiscountType(), |
| 805 |
'_tpt_applying_type' => $this->getApplyingType(), |
| 806 |
'_tpt_tiered_pricing_type' => $this->getTieredPricingType(), |
| 807 |
'_tpt_percentage_rules' => $this->getPercentageTieredPricingRules(), |
| 808 |
'_tpt_fixed_rules' => $this->getFixedTieredPricingRules(), |
| 809 |
'_tpt_minimum' => $this->getMinimum(), |
| 810 |
'_tpt_mix_and_match_minimum' => ( is_null( $this->getMixAndMatchMinQuantity() ) ? '' : wc_bool_to_string( $this->getMixAndMatchMinQuantity() ) ), |
| 811 |
'_tpt_tax_status' => $this->getTaxStatus(), |
| 812 |
'_tpt_tax_class' => $this->getTaxClass(), |
| 813 |
'_tpt_priority' => $this->getPriority(), |
| 814 |
'_tpt_start_date' => (string) $this->getStartDate(), |
| 815 |
'_tpt_end_date' => (string) $this->getEndDate(), |
| 816 |
'_tpt_included_categories' => $this->getIncludedProductCategories(), |
| 817 |
'_tpt_include_subcategories' => wc_bool_to_string( $this->isIncludeSubcategories() ), |
| 818 |
'_tpt_included_tags' => $this->getIncludedProductTags(), |
| 819 |
'_tpt_included_brands' => $this->getIncludedProductBrands(), |
| 820 |
'_tpt_included_products' => $this->getIncludedProducts(), |
| 821 |
'_tpt_included_users' => $this->getIncludedUsers(), |
| 822 |
'_tpt_included_user_roles' => $this->getIncludedUserRoles(), |
| 823 |
'_tpt_excluded_categories' => $this->getExcludedProductCategories(), |
| 824 |
'_tpt_excluded_tags' => $this->getExcludedProductTags(), |
| 825 |
'_tpt_excluded_brands' => $this->getExcludedProductBrands(), |
| 826 |
'_tpt_excluded_products' => $this->getExcludedProducts(), |
| 827 |
'_tpt_excluded_users' => $this->getExcludedUsers(), |
| 828 |
'_tpt_excluded_user_roles' => $this->getExcludedUserRoles(), |
| 829 |
'_tpt_is_suspended' => wc_bool_to_string( $this->isSuspended() ), |
| 830 |
); |
| 831 |
foreach ( $dataToUpdate as $key => $value ) { |
| 832 |
update_post_meta( $this->getId(), $key, $value ); |
| 833 |
} |
| 834 |
} |
| 835 |
|
| 836 |
public static function build( $ruleId ) : self { |
| 837 |
// Simple data to read |
| 838 |
$dataToRead = array( |
| 839 |
'_tpt_pricing_type' => 'pricing_type', |
| 840 |
'_tpt_sale_price' => 'sale_price', |
| 841 |
'_tpt_regular_price' => 'regular_price', |
| 842 |
'_tpt_discount' => 'discount', |
| 843 |
'_tpt_discount_type' => 'discount_type', |
| 844 |
'_tpt_applying_type' => 'applying_type', |
| 845 |
'_tpt_tiered_pricing_type' => 'tiered_pricing_type', |
| 846 |
'_tpt_minimum' => 'minimum', |
| 847 |
'_tpt_mix_and_match_minimum' => 'mix_and_match_minimum', |
| 848 |
'_tpt_tax_status' => 'tax_status', |
| 849 |
'_tpt_tax_class' => 'tax_class', |
| 850 |
'_tpt_is_suspended' => 'is_suspended', |
| 851 |
'_tpt_priority' => 'priority', |
| 852 |
'_tpt_start_date' => 'start_date', |
| 853 |
'_tpt_end_date' => 'end_date', |
| 854 |
); |
| 855 |
$data = array(); |
| 856 |
foreach ( $dataToRead as $key => $name ) { |
| 857 |
$data[$name] = get_post_meta( $ruleId, $key, true ); |
| 858 |
} |
| 859 |
$priceRule = self::fromArray( $data ); |
| 860 |
$priceRule->setCreatedAt( (string) get_post_field( 'post_date_gmt', $ruleId ) ); |
| 861 |
$existingRoles = wp_roles()->roles; |
| 862 |
$includedCategoriesIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_included_categories', true ) ) ); |
| 863 |
$includedTagsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_included_tags', true ) ) ); |
| 864 |
$includedBrandsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_included_brands', true ) ) ); |
| 865 |
$includedProductsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_included_products', true ) ) ); |
| 866 |
$includedUsersRole = array_filter( (array) get_post_meta( $ruleId, '_tpt_included_user_roles', true ), array(self::class, 'isSelectableRole') ); |
| 867 |
$includedUsers = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_included_users', true ) ) ); |
| 868 |
$excludedCategoriesIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_excluded_categories', true ) ) ); |
| 869 |
$excludedTagsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_excluded_tags', true ) ) ); |
| 870 |
$excludedBrandsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_excluded_brands', true ) ) ); |
| 871 |
$excludedProductsIds = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_excluded_products', true ) ) ); |
| 872 |
$excludedUsersRole = array_filter( (array) get_post_meta( $ruleId, '_tpt_excluded_user_roles', true ), array(self::class, 'isSelectableRole') ); |
| 873 |
$excludedUsers = array_filter( array_map( 'intval', (array) get_post_meta( $ruleId, '_tpt_excluded_users', true ) ) ); |
| 874 |
$isSuspended = get_post_meta( $ruleId, '_tpt_is_suspended', true ) === 'yes'; |
| 875 |
$priceRule->setPercentageTieredPricingRules( self::readPricingRules( 'percentage', $ruleId ) ); |
| 876 |
$priceRule->setFixedTieredPricingRules( self::readPricingRules( 'fixed', $ruleId ) ); |
| 877 |
$priceRule->setIncludedProductCategories( $includedCategoriesIds ); |
| 878 |
$priceRule->setIncludeSubcategories( 'yes' === get_post_meta( $ruleId, '_tpt_include_subcategories', true ) ); |
| 879 |
$priceRule->setIncludedProductTags( $includedTagsIds ); |
| 880 |
$priceRule->setIncludedProductBrands( $includedBrandsIds ); |
| 881 |
$priceRule->setIncludedUsers( $includedUsers ); |
| 882 |
$priceRule->setIncludedUsersRole( $includedUsersRole ); |
| 883 |
$priceRule->setIncludedProducts( $includedProductsIds ); |
| 884 |
$priceRule->setExcludedProductCategories( $excludedCategoriesIds ); |
| 885 |
$priceRule->setExcludedProductTags( $excludedTagsIds ); |
| 886 |
$priceRule->setExcludedProductBrands( $excludedBrandsIds ); |
| 887 |
$priceRule->setExcludedUsers( $excludedUsers ); |
| 888 |
$priceRule->setExcludedUsersRole( $excludedUsersRole ); |
| 889 |
$priceRule->setExcludedProducts( $excludedProductsIds ); |
| 890 |
$priceRule->setIsSuspended( $isSuspended ); |
| 891 |
$priceRule->setId( $ruleId ); |
| 892 |
return apply_filters( 'tiered_pricing_table/global_pricing/after_built_rule', $priceRule ); |
| 893 |
} |
| 894 |
|
| 895 |
protected static function readPricingRules( $type, $id ) : array { |
| 896 |
$type = ( in_array( $type, array('percentage', 'fixed') ) ? $type : 'fixed' ); |
| 897 |
$rules = get_post_meta( $id, "_tpt_{$type}_rules", true ); |
| 898 |
$rules = ( !empty( $rules ) ? $rules : array() ); |
| 899 |
$rules = ( is_array( $rules ) ? array_filter( $rules ) : array() ); |
| 900 |
ksort( $rules ); |
| 901 |
return $rules; |
| 902 |
} |
| 903 |
|
| 904 |
public function setIsSuspended( bool $isSuspended ) { |
| 905 |
$this->isSuspended = $isSuspended; |
| 906 |
} |
| 907 |
|
| 908 |
public function suspend() { |
| 909 |
$this->setIsSuspended( true ); |
| 910 |
} |
| 911 |
|
| 912 |
public function reactivate() { |
| 913 |
$this->setIsSuspended( false ); |
| 914 |
} |
| 915 |
|
| 916 |
public function isSuspended() : bool { |
| 917 |
return $this->isSuspended; |
| 918 |
} |
| 919 |
|
| 920 |
/** |
| 921 |
* Wrapper for the main "match" function to provide the hook for 3rd party devs |
| 922 |
* |
| 923 |
* @param WP_User $user |
| 924 |
* @param WC_Product $product |
| 925 |
* |
| 926 |
* @return bool |
| 927 |
*/ |
| 928 |
public function matchRequirements( WP_User $user, WC_Product $product ) : bool { |
| 929 |
$matched = $this->_matchRequirements( $user, $product ); |
| 930 |
return apply_filters( |
| 931 |
'tiered_pricing_table/global_pricing/match_requirements', |
| 932 |
$matched, |
| 933 |
$this, |
| 934 |
$user, |
| 935 |
$product |
| 936 |
); |
| 937 |
} |
| 938 |
|
| 939 |
protected function _matchRequirements( WP_User $user, WC_Product $product ) : bool { |
| 940 |
$parentProduct = ( $product->is_type( array('variation', 'subscription-variation') ) ? wc_get_product( $product->get_parent_id() ) : $product ); |
| 941 |
/** |
| 942 |
* 1. Check for product exclusion |
| 943 |
* |
| 944 |
* If the product in exclusion - pricing rule does not match immediately |
| 945 |
*/ |
| 946 |
$excludedProducts = $this->translateIds( $this->getExcludedProducts(), 'product' ); |
| 947 |
if ( !empty( $excludedProducts ) ) { |
| 948 |
if ( in_array( $product->get_id(), $excludedProducts ) || in_array( $parentProduct->get_id(), $excludedProducts ) ) { |
| 949 |
return false; |
| 950 |
} |
| 951 |
} |
| 952 |
$excludedProductCategories = $this->translateIds( $this->getEffectiveExcludedCategories(), 'product_cat' ); |
| 953 |
if ( !empty( $excludedProductCategories ) ) { |
| 954 |
if ( !empty( array_intersect( $parentProduct->get_category_ids(), $excludedProductCategories ) ) ) { |
| 955 |
return false; |
| 956 |
} |
| 957 |
} |
| 958 |
$excludedProductTags = $this->translateIds( $this->getExcludedProductTags(), 'product_tag' ); |
| 959 |
if ( !empty( $excludedProductTags ) ) { |
| 960 |
if ( !empty( array_intersect( $parentProduct->get_tag_ids(), $excludedProductTags ) ) ) { |
| 961 |
return false; |
| 962 |
} |
| 963 |
} |
| 964 |
$excludedProductBrands = $this->translateIds( $this->getExcludedProductBrands(), 'product_brand' ); |
| 965 |
if ( !empty( $excludedProductBrands ) ) { |
| 966 |
$productBrands = wp_get_post_terms( $parentProduct->get_id(), 'product_brand', array( |
| 967 |
'fields' => 'ids', |
| 968 |
) ); |
| 969 |
if ( is_wp_error( $productBrands ) ) { |
| 970 |
$productBrands = []; |
| 971 |
} |
| 972 |
if ( !empty( array_intersect( $productBrands, $excludedProductBrands ) ) ) { |
| 973 |
return false; |
| 974 |
} |
| 975 |
} |
| 976 |
/** |
| 977 |
* 2. Check for user exclusion |
| 978 |
* |
| 979 |
* If users in exclusion - pricing rule does not match immediately |
| 980 |
*/ |
| 981 |
if ( in_array( $user->ID, $this->getExcludedUsers() ) ) { |
| 982 |
return false; |
| 983 |
} |
| 984 |
$userRoles = self::rolesOf( $user ); |
| 985 |
foreach ( $this->getExcludedUserRoles() as $role ) { |
| 986 |
if ( in_array( $role, $userRoles ) ) { |
| 987 |
return false; |
| 988 |
} |
| 989 |
} |
| 990 |
/** |
| 991 |
* 3. Check for rule limitation for specific products |
| 992 |
* |
| 993 |
* If yes - match rule only for selected product/product categories |
| 994 |
*/ |
| 995 |
$productMatched = false; |
| 996 |
$productLimitations = false; |
| 997 |
$includedProducts = $this->translateIds( $this->getIncludedProducts(), 'product' ); |
| 998 |
if ( !empty( $includedProducts ) ) { |
| 999 |
$productLimitations = true; |
| 1000 |
if ( in_array( $product->get_id(), $includedProducts ) || in_array( $parentProduct->get_id(), $includedProducts ) ) { |
| 1001 |
$productMatched = true; |
| 1002 |
} |
| 1003 |
} |
| 1004 |
$includedProductCategories = $this->translateIds( $this->getEffectiveIncludedCategories(), 'product_cat' ); |
| 1005 |
if ( !empty( $includedProductCategories ) ) { |
| 1006 |
$productLimitations = true; |
| 1007 |
if ( !empty( array_intersect( $parentProduct->get_category_ids(), $includedProductCategories ) ) ) { |
| 1008 |
$productMatched = true; |
| 1009 |
} |
| 1010 |
} |
| 1011 |
$includedProductTags = $this->translateIds( $this->getIncludedProductTags(), 'product_tag' ); |
| 1012 |
if ( !empty( $includedProductTags ) ) { |
| 1013 |
$productLimitations = true; |
| 1014 |
if ( !empty( array_intersect( $parentProduct->get_tag_ids(), $includedProductTags ) ) ) { |
| 1015 |
$productMatched = true; |
| 1016 |
} |
| 1017 |
} |
| 1018 |
$includedProductBrands = $this->translateIds( $this->getIncludedProductBrands(), 'product_brand' ); |
| 1019 |
if ( !empty( $includedProductBrands ) ) { |
| 1020 |
$productLimitations = true; |
| 1021 |
$productBrands = wp_get_post_terms( $parentProduct->get_id(), 'product_brand', array( |
| 1022 |
'fields' => 'ids', |
| 1023 |
) ); |
| 1024 |
if ( is_wp_error( $productBrands ) ) { |
| 1025 |
$productBrands = []; |
| 1026 |
} |
| 1027 |
if ( !empty( array_intersect( $productBrands, $includedProductBrands ) ) ) { |
| 1028 |
$productMatched = true; |
| 1029 |
} |
| 1030 |
} |
| 1031 |
// There is product limitation and the product/category does not match the rule |
| 1032 |
if ( $productLimitations && !$productMatched ) { |
| 1033 |
return false; |
| 1034 |
} |
| 1035 |
/** |
| 1036 |
* 4. If there are no user limits - match the rule immediately |
| 1037 |
*/ |
| 1038 |
if ( empty( $this->getIncludedUserRoles() ) && empty( $this->getIncludedUsers() ) ) { |
| 1039 |
return true; |
| 1040 |
} |
| 1041 |
/** |
| 1042 |
* 5. If there are user limits - check for user ID and user role. |
| 1043 |
*/ |
| 1044 |
if ( in_array( $user->ID, $this->getIncludedUsers() ) ) { |
| 1045 |
return true; |
| 1046 |
} |
| 1047 |
foreach ( $this->getIncludedUserRoles() as $role ) { |
| 1048 |
if ( in_array( $role, $userRoles ) ) { |
| 1049 |
return true; |
| 1050 |
} |
| 1051 |
} |
| 1052 |
return false; |
| 1053 |
} |
| 1054 |
|
| 1055 |
/** |
| 1056 |
* Map a rule's stored object IDs to the language currently being viewed. |
| 1057 |
* |
| 1058 |
* A rule is created in a single language and stores the product/term IDs of that language. The |
| 1059 |
* product being priced (and the term IDs returned by WooCommerce for it) are in the current |
| 1060 |
* language, so under WPML/Polylang a raw ID comparison would never match on a translation. This |
| 1061 |
* translates the stored IDs into the current language via the shared `wpml_object_id` filter |
| 1062 |
* (implemented by both WPML and Polylang) so the comparison works across every translation. |
| 1063 |
* |
| 1064 |
* When no multilingual plugin is active the filter has no listeners, so the IDs are returned |
| 1065 |
* unchanged with no overhead. |
| 1066 |
* |
| 1067 |
* @param int[] $ids Object IDs as stored on the rule. |
| 1068 |
* @param string $type Element type: a post type ('product') or taxonomy ('product_cat', |
| 1069 |
* 'product_tag', 'product_brand'). |
| 1070 |
* |
| 1071 |
* @return int[] |
| 1072 |
*/ |
| 1073 |
protected function translateIds( array $ids, string $type ) : array { |
| 1074 |
if ( empty( $ids ) || !has_filter( 'wpml_object_id' ) ) { |
| 1075 |
return $ids; |
| 1076 |
} |
| 1077 |
$translated = array(); |
| 1078 |
foreach ( $ids as $id ) { |
| 1079 |
// The `true` argument keeps the original ID when the object has no translation in the |
| 1080 |
// current language, preserving the previous behaviour for untranslated objects. |
| 1081 |
$translated[] = (int) apply_filters( |
| 1082 |
'wpml_object_id', |
| 1083 |
$id, |
| 1084 |
$type, |
| 1085 |
true |
| 1086 |
); |
| 1087 |
} |
| 1088 |
return $translated; |
| 1089 |
} |
| 1090 |
|
| 1091 |
} |
| 1092 |
|