feat(Rule): add new rules

This commit is contained in:
2026-05-09 21:24:10 +03:30
parent fd9d29a0ee
commit aa944bf339
9 changed files with 532 additions and 178 deletions

View File

@@ -51,9 +51,7 @@ class PricingService {
$oldPrice = $price;
$price = $this->applyRuleActions($rule, $price);
if ($price < $oldPrice) {
$this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
}
$this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
}
$price = $this->enforceLimits($originalPrice, $price);
@@ -62,20 +60,16 @@ class PricingService {
}
private function getApplicableRules($product) {
$cache_key = 'applicable_rules_' . ($product ? $product->get_id() : 'all');
return $this->cache->remember($cache_key, function() use ($product) {
$rules = $this->ruleRepository->getEnabled();
$applicable = [];
$rules = $this->ruleRepository->getEnabled();
$applicable = [];
foreach ($rules as $rule) {
if ($this->ruleMatches($rule, $product)) {
$applicable[] = $rule;
}
foreach ($rules as $rule) {
if ($this->ruleMatches($rule, $product)) {
$applicable[] = $rule;
}
}
return $applicable;
}, 300, 'pricing');
return $applicable;
}
private function chooseRule(array $rules, $price) {
@@ -139,7 +133,7 @@ class PricingService {
switch ($type) {
case 'user_type':
return $this->getUserType() === $value;
return $this->compareAnyValue($this->getUserTypeAliases(), $value, $condition['operator'] ?? 'is');
case 'cart_total_min':
return $this->getCartTotal() >= floatval($value);
case 'cart_total_max':
@@ -150,8 +144,24 @@ class PricingService {
return $this->getCartItemCount() <= intval($value);
case 'product_category':
return $this->productHasCategory($product, (array) $value);
case 'exclude_product_category':
return !$this->productHasCategory($product, (array) $value);
case 'product_tag':
return $this->productHasTerm($product, (array) $value, 'product_tag');
case 'product_ids':
return $this->productIsInIds($product, (array) $value);
case 'exclude_product_ids':
return !$this->productIsInIds($product, (array) $value);
case 'cart_contains_product':
return $this->cartContainsProduct((array) $value);
case 'cart_contains_category':
return $this->cartContainsCategory((array) $value);
case 'customer_order_count_min':
return $this->getCustomerOrderCount() >= intval($value);
case 'customer_order_count_max':
return $this->getCustomerOrderCount() <= intval($value);
case 'day_of_week':
return in_array((string) current_time('N'), array_map('strval', $this->normalizeIdList($value)), true);
default:
return true;
}
@@ -168,6 +178,23 @@ class PricingService {
return $order_count > 0 ? 'returning' : 'new';
}
private function getUserTypeAliases() {
$type = $this->getUserType();
$aliases = [$type];
if (is_user_logged_in()) {
$aliases[] = 'logged_in';
}
return $aliases;
}
private function getCustomerOrderCount() {
if (!is_user_logged_in()) {
return 0;
}
return (int) wc_get_customer_order_count(get_current_user_id());
}
private function userHasAllowedRole($roles) {
if (!is_user_logged_in()) {
return false;
@@ -197,16 +224,20 @@ class PricingService {
}
private function productHasCategory($product, $categories) {
if (!$product || empty($categories)) {
return $this->productHasTerm($product, $categories, 'product_cat');
}
private function productHasTerm($product, $terms, $taxonomy) {
if (!$product || empty($terms)) {
return false;
}
$product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
if (is_wp_error($product_cats)) {
$product_terms = wp_get_post_terms($product->get_id(), $taxonomy, ['fields' => 'ids']);
if (is_wp_error($product_terms)) {
return false;
}
return (bool) array_intersect($product_cats, $this->normalizeIdList($categories));
return (bool) array_intersect($product_terms, $this->normalizeIdList($terms));
}
private function productIsInIds($product, $ids) {
@@ -214,7 +245,43 @@ class PricingService {
return false;
}
return in_array($product->get_id(), $this->normalizeIdList($ids), true);
$ids = $this->normalizeIdList($ids);
$productIds = [(int) $product->get_id()];
if ($product->is_type('variation') && method_exists($product, 'get_parent_id')) {
$productIds[] = (int) $product->get_parent_id();
}
return (bool) array_intersect($productIds, $ids);
}
private function cartContainsProduct($ids) {
if (!function_exists('WC') || !WC()->cart) {
return false;
}
$ids = $this->normalizeIdList($ids);
foreach (WC()->cart->get_cart() as $cartItem) {
if (in_array((int) $cartItem['product_id'], $ids, true) || in_array((int) $cartItem['variation_id'], $ids, true)) {
return true;
}
}
return false;
}
private function cartContainsCategory($categories) {
if (!function_exists('WC') || !WC()->cart) {
return false;
}
foreach (WC()->cart->get_cart() as $cartItem) {
$product = wc_get_product($cartItem['product_id']);
if ($this->productHasCategory($product, $categories)) {
return true;
}
}
return false;
}
private function normalizeIdList($value) {
@@ -249,7 +316,7 @@ class PricingService {
if ($value <= 0) {
return $price;
}
return $price * (1 - $value / 100);
return $price * (1 - min(100, $value) / 100);
case 'discount_fixed':
if ($value <= 0) {
return $price;
@@ -257,6 +324,10 @@ class PricingService {
return $price - $value;
case 'set_price':
return $value > 0 ? $value : $price;
case 'increase_percent':
return $value > 0 ? $price * (1 + min(100, $value) / 100) : $price;
case 'increase_fixed':
return $value > 0 ? $price + $value : $price;
case 'free_shipping':
return $price;
default:
@@ -290,11 +361,78 @@ class PricingService {
return 0;
}
public function applyFreeShippingRates($rates) {
$rules = $this->getApplicableRules(null);
$hasFreeShippingRule = false;
foreach ($rules as $rule) {
foreach ($rule->actions as $action) {
if (($action['type'] ?? '') === 'free_shipping') {
$hasFreeShippingRule = true;
break 2;
}
}
}
if (!$hasFreeShippingRule) {
return $rates;
}
foreach ($rates as $rate) {
$rate->cost = 0;
if (!empty($rate->taxes) && is_array($rate->taxes)) {
foreach ($rate->taxes as $taxId => $tax) {
$rate->taxes[$taxId] = 0;
}
}
}
return $rates;
}
private function compareValue($actual, $expected, $operator) {
$expectedValues = array_map('trim', explode(',', (string) $expected));
$actual = (string) $actual;
switch ($operator) {
case 'is_not':
return $actual !== (string) $expected;
case 'in':
return in_array($actual, $expectedValues, true);
case 'not_in':
return !in_array($actual, $expectedValues, true);
case 'is':
default:
return $actual === (string) $expected;
}
}
private function compareAnyValue(array $actualValues, $expected, $operator) {
$expectedValues = array_map('trim', explode(',', (string) $expected));
if (in_array($operator, ['is_not', 'not_in'], true)) {
foreach ($actualValues as $actual) {
if (in_array((string) $actual, $expectedValues, true)) {
return false;
}
}
return true;
}
foreach ($actualValues as $actual) {
if (in_array((string) $actual, $expectedValues, true)) {
return true;
}
}
return false;
}
private function trackDiscountOnce($product, $oldPrice, $price, $ruleId) {
$productId = $product ? $product->get_id() : 0;
$key = implode(':', [$productId, (int) $ruleId, round($oldPrice, 4), round($price, 4)]);
if (isset($this->trackedApplications[$key])) {
if ($price >= $oldPrice || isset($this->trackedApplications[$key])) {
return;
}