array( 'type' => 'product', 'label' => __( 'Order includes item', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_categories' => array( 'type' => 'term_cat', 'label' => __( 'Order includes category', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_brands' => array( 'type' => 'term_brand', 'label' => __( 'Order includes brand', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_tags' => array( 'type' => 'term_tag', 'label' => __( 'Order includes tagged item', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_needs' => array( 'type' => 'term_need', 'label' => __( 'Order includes item for need', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_total' => array( 'type' => 'number', 'label' => sprintf( __( 'Order total (%s)', 'ai-eshop-optimizer' ), function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ) : '€' ), 'ops' => array( '>', '<', '>=', '<=', '=' ) ), 'order_item_count' => array( 'type' => 'number', 'label' => __( 'Distinct line items', 'ai-eshop-optimizer' ), 'ops' => array( '>', '<', '>=', '<=', '=' ) ), 'customer_role' => array( 'type' => 'role', 'label' => __( 'Customer role', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not' ) ), 'order_taxonomy' => array( 'type' => 'taxonomy', 'label' => __( 'Order includes product from taxonomy term', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_variation_attribute' => array( 'type' => 'var_attr', 'label' => __( 'Order includes variation with attribute', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_qty_count' => array( 'type' => 'number', 'label' => __( 'Total quantity (sum of items)', 'ai-eshop-optimizer' ), 'ops' => array( '>', '<', '>=', '<=', '=' ) ), 'order_payment_gateway' => array( 'type' => 'string', 'label' => __( 'Payment method', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not', 'in' ) ), 'order_billing_country' => array( 'type' => 'string', 'label' => __( 'Billing country', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not', 'in' ) ), 'order_shipping_country'=> array( 'type' => 'string', 'label' => __( 'Shipping country', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not', 'in' ) ), 'order_created_via' => array( 'type' => 'string', 'label' => __( 'Created via', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not', 'in' ) ), 'order_coupons' => array( 'type' => 'coupon', 'label' => __( 'Order used coupon', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_shipping_method' => array( 'type' => 'string', 'label' => __( 'Shipping method', 'ai-eshop-optimizer' ), 'ops' => array( 'includes', 'not_includes' ) ), 'order_is_first' => array( 'type' => 'bool', 'label' => __( 'Is customer\'s first order', 'ai-eshop-optimizer' ), 'ops' => array( 'is', 'is_not' ) ), 'order_billing_email' => array( 'type' => 'string', 'label' => __( 'Billing email', 'ai-eshop-optimizer' ), 'ops' => array( 'contains', 'not_contains', 'is', 'is_not' ) ), ); } public static function matches( $order, $rule ) { if ( ! is_array( $rule ) || empty( $rule['conditions'] ) ) { return true; } if ( ! ( $order instanceof WC_Order ) ) { $order = function_exists( 'wc_get_order' ) ? wc_get_order( (int) $order ) : null; } if ( ! $order instanceof WC_Order ) { return false; } return self::eval_tree( $rule, self::order_context( $order ) ); } public static function matches_cart( $items, $rule, $extra = array() ) { if ( ! is_array( $rule ) || empty( $rule['conditions'] ) ) { return true; } return self::eval_tree( $rule, self::build_cart_context( $items, $extra ) ); } private static function build_cart_context( $items, $extra = array() ) { $products = array(); $cats = array(); $brands = array(); $parent_ids = array(); $variation_ids = array(); $brand_tax = self::brand_tax(); $qty = 0; $lines = 0; foreach ( (array) $items as $it ) { $pid = (int) ( $it['product_id'] ?? 0 ); $vid = (int) ( $it['variation_id'] ?? 0 ); $q = (int) ( $it['quantity'] ?? 1 ); if ( $pid <= 0 && $vid <= 0 ) { continue; } $lines++; $qty += max( 1, $q ); if ( $pid > 0 ) { $products[ $pid ] = true; $parent_ids[ $pid ] = true; } if ( $vid > 0 ) { $products[ $vid ] = true; $variation_ids[ $vid ] = true; } $term_pid = $pid > 0 ? $pid : $vid; foreach ( (array) wp_get_post_terms( $term_pid, self::CATEGORY_TAX, array( 'fields' => 'ids' ) ) as $t ) { $cats[ (int) $t ] = true; } if ( '' !== $brand_tax && taxonomy_exists( $brand_tax ) ) { foreach ( (array) wp_get_post_terms( $term_pid, $brand_tax, array( 'fields' => 'ids' ) ) as $t ) { $brands[ (int) $t ] = true; } } } return array( 'products' => $products, 'categories' => $cats, 'brands' => $brands, 'parent_ids' => $parent_ids, 'variation_ids' => $variation_ids, 'total' => (float) ( $extra['total'] ?? 0 ), 'item_count' => $lines, 'qty_count' => $qty, 'payment_gateway' => '', 'billing_country' => '', 'shipping_country' => '', 'created_via' => '', 'coupons' => array_map( 'strtolower', (array) ( $extra['coupons'] ?? array() ) ), 'shipping_methods' => array(), 'billing_email' => strtolower( trim( (string) ( $extra['email'] ?? '' ) ) ), 'customer_id' => 0, 'order_id' => 0, 'created_ts' => 0, ); } private static function eval_tree( $rule, $ctx ) { $op = ( 'or' === strtolower( (string) ( $rule['op'] ?? 'and' ) ) ) ? 'or' : 'and'; $any_seen = false; foreach ( (array) $rule['conditions'] as $c ) { if ( ! is_array( $c ) ) { continue; } $any_seen = true; $r = isset( $c['conditions'] ) ? self::eval_tree( $c, $ctx ) : self::eval_leaf( $c, $ctx ); if ( 'and' === $op && ! $r ) { return false; } if ( 'or' === $op && $r ) { return true; } } return ( 'and' === $op ) || ! $any_seen; } private static function order_context( $order ) { $products = array(); $cats = array(); $brands = array(); $parent_ids = array(); $variation_ids = array(); $brand_tax = self::brand_tax(); $items = $order->get_items(); foreach ( $items as $item ) { if ( ! is_object( $item ) || ! method_exists( $item, 'get_product_id' ) ) { continue; } $pid = (int) $item->get_product_id(); $vid = (int) $item->get_variation_id(); if ( $pid > 0 ) { $products[ $pid ] = true; $parent_ids[ $pid ] = true; } if ( $vid > 0 ) { $products[ $vid ] = true; $variation_ids[ $vid ] = true; } $term_pid = $pid > 0 ? $pid : $vid; if ( $term_pid > 0 ) { foreach ( (array) wp_get_post_terms( $term_pid, self::CATEGORY_TAX, array( 'fields' => 'ids' ) ) as $t ) { $cats[ (int) $t ] = true; } if ( '' !== $brand_tax && taxonomy_exists( $brand_tax ) ) { foreach ( (array) wp_get_post_terms( $term_pid, $brand_tax, array( 'fields' => 'ids' ) ) as $t ) { $brands[ (int) $t ] = true; } } } } $roles = array(); $uid = (int) $order->get_customer_id(); if ( $uid > 0 ) { $u = get_userdata( $uid ); if ( $u ) { $roles = array_map( 'strval', (array) $u->roles ); } } $ship_methods = array(); if ( method_exists( $order, 'get_shipping_methods' ) ) { foreach ( (array) $order->get_shipping_methods() as $sm ) { if ( is_object( $sm ) ) { if ( method_exists( $sm, 'get_method_id' ) ) { $ship_methods[] = strtolower( (string) $sm->get_method_id() ); } if ( method_exists( $sm, 'get_name' ) ) { $ship_methods[] = strtolower( (string) $sm->get_name() ); } } } } return array( 'products' => $products, 'categories' => $cats, 'brands' => $brands, 'parent_ids' => $parent_ids, 'variation_ids' => $variation_ids, 'total' => (float) ( $order->get_total() - (float) $order->get_total_refunded() ), 'item_count' => count( $items ), 'qty_count' => (int) ( method_exists( $order, 'get_item_count' ) ? $order->get_item_count() : count( $items ) ), 'payment_gateway' => strtolower( (string) $order->get_payment_method() ), 'billing_country' => strtoupper( (string) $order->get_billing_country() ), 'shipping_country' => strtoupper( (string) $order->get_shipping_country() ), 'created_via' => strtolower( (string) ( method_exists( $order, 'get_created_via' ) ? $order->get_created_via() : '' ) ), 'coupons' => array_map( 'strtolower', (array) ( method_exists( $order, 'get_coupon_codes' ) ? $order->get_coupon_codes() : array() ) ), 'shipping_methods' => $ship_methods, 'roles' => $roles, 'billing_email' => strtolower( trim( (string) $order->get_billing_email() ) ), 'customer_id' => $uid, 'order_id' => (int) $order->get_id(), 'created_ts' => ( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : time() ), ); } private static function eval_leaf( $c, $ctx ) { $field = (string) ( $c['field'] ?? '' ); $op = strtolower( (string) ( $c['op'] ?? '' ) ); $val = $c['value'] ?? null; switch ( $field ) { case 'order_items': return self::set_op( $ctx['products'], self::ints( $val ), $op ); case 'order_categories': return self::set_op( $ctx['categories'], self::ints( $val ), $op ); case 'order_brands': return self::set_op( $ctx['brands'], self::ints( $val ), $op ); case 'order_total': return self::num_op( $ctx['total'], (float) $val, $op ); case 'order_item_count': return self::num_op( (float) $ctx['item_count'], (float) $val, $op ); case 'customer_role': $wanted = array_map( 'strval', is_array( $val ) ? $val : array( $val ) ); $has = (bool) array_intersect( $ctx['roles'], $wanted ); return ( 'is_not' === $op ) ? ! $has : $has; case 'order_taxonomy': return self::taxonomy_op( $ctx, $val, $op ); case 'order_tags': return self::taxonomy_op( $ctx, array( 'tax' => self::tag_taxonomy(), 'ids' => self::ints( $val ) ), $op ); case 'order_needs': return self::taxonomy_op( $ctx, array( 'tax' => self::needs_taxonomy(), 'ids' => self::ints( $val ) ), $op ); case 'order_variation_attribute': return self::variation_attr_op( $ctx, $val, $op ); case 'order_qty_count': return self::num_op( (float) $ctx['qty_count'], (float) $val, $op ); case 'order_payment_gateway': return self::str_scalar_op( $ctx['payment_gateway'], $val, $op ); case 'order_billing_country': return self::str_scalar_op( $ctx['billing_country'], $val, $op, true ); case 'order_shipping_country': return self::str_scalar_op( $ctx['shipping_country'], $val, $op, true ); case 'order_created_via': return self::str_scalar_op( $ctx['created_via'], $val, $op ); case 'order_coupons': return self::str_set_op( $ctx['coupons'], $val, $op ); case 'order_shipping_method': return self::str_set_op( $ctx['shipping_methods'], $val, $op, true ); case 'order_is_first': $first = self::is_first_order( $ctx ); return ( 'is_not' === $op ) ? ( $first !== (bool) $val ) : ( $first === (bool) $val ); case 'order_billing_email': return self::email_op( isset( $ctx['billing_email'] ) ? $ctx['billing_email'] : '', $val, $op ); } return false; } private static function is_first_order( $ctx ) { $email = isset( $ctx['billing_email'] ) ? (string) $ctx['billing_email'] : ''; $uid = (int) ( $ctx['customer_id'] ?? 0 ); $oid = (int) ( $ctx['order_id'] ?? 0 ); $ts = (int) ( $ctx['created_ts'] ?? 0 ); if ( ( '' === $email && $uid <= 0 ) || $oid <= 0 || $ts <= 0 || ! function_exists( 'wc_get_orders' ) ) { return false; } $customer = array(); if ( '' !== $email ) { $customer[] = $email; } if ( $uid > 0 ) { $customer[] = $uid; } $statuses = array_diff( array_keys( wc_get_order_statuses() ), array( 'wc-checkout-draft' ) ); $prior = wc_get_orders( array( 'type' => 'shop_order', 'status' => $statuses, 'customer' => $customer, 'exclude' => array( $oid ), 'date_created' => '<' . $ts, 'limit' => 1, 'return' => 'ids', ) ); return empty( $prior ); } private static function email_op( $have, $val, $op ) { $have = strtolower( trim( (string) $have ) ); $vals = array(); foreach ( ( is_array( $val ) ? $val : array( $val ) ) as $v ) { $v = strtolower( trim( (string) $v ) ); if ( '' !== $v ) { $vals[] = $v; } } if ( 'is' === $op ) { return in_array( $have, $vals, true ); } if ( 'is_not' === $op ) { return ! in_array( $have, $vals, true ); } $hit = false; foreach ( $vals as $needle ) { if ( '' !== $have && false !== strpos( $have, $needle ) ) { $hit = true; break; } } return ( 'not_contains' === $op ) ? ! $hit : $hit; } private static function str_scalar_op( $have, $val, $op, $upper = false ) { $norm = function ( $s ) use ( $upper ) { $s = trim( (string) $s ); return $upper ? strtoupper( $s ) : strtolower( $s ); }; $have = $norm( $have ); if ( 'in' === $op ) { $set = array_map( $norm, is_array( $val ) ? $val : array( $val ) ); return in_array( $have, $set, true ); } $want = $norm( is_array( $val ) ? reset( $val ) : $val ); return ( 'is_not' === $op ) ? ( $have !== $want ) : ( $have === $want ); } private static function str_set_op( $have_set, $val, $op, $substr = false ) { $want = array_map( function ( $s ) { return strtolower( trim( (string) $s ) ); }, is_array( $val ) ? $val : array( $val ) ); $want = array_filter( $want ); $hit = false; foreach ( (array) $have_set as $h ) { $h = strtolower( (string) $h ); foreach ( $want as $w ) { if ( $h === $w || ( $substr && '' !== $w && false !== strpos( $h, $w ) ) ) { $hit = true; break 2; } } } return ( 'not_includes' === $op ) ? ! $hit : $hit; } private static function tag_taxonomy() { return (string) apply_filters( 'aieo_purchases_tag_taxonomy', 'product_tag' ); } private static function needs_taxonomy() { return (string) apply_filters( 'aieo_purchases_needs_taxonomy', 'pa_need' ); } private static function taxonomy_op( $ctx, $val, $op ) { $val = is_array( $val ) ? $val : array(); $tax = sanitize_key( (string) ( $val['tax'] ?? $val['taxonomy'] ?? '' ) ); $ids = self::ints( $val['ids'] ?? ( $val['terms'] ?? array() ) ); if ( '' === $tax || empty( $ids ) || ! function_exists( 'taxonomy_exists' ) || ! taxonomy_exists( $tax ) ) { return false; } $hit = false; foreach ( array_keys( (array) $ctx['parent_ids'] ) as $pid ) { $terms = wp_get_post_terms( (int) $pid, $tax, array( 'fields' => 'ids' ) ); if ( ! is_wp_error( $terms ) && array_intersect( $ids, array_map( 'intval', (array) $terms ) ) ) { $hit = true; break; } } return ( 'not_includes' === $op ) ? ! $hit : $hit; } private static function variation_attr_op( $ctx, $val, $op ) { $val = is_array( $val ) ? $val : array(); $attr = strtolower( ltrim( (string) ( $val['attribute'] ?? '' ), '' ) ); $want = strtolower( (string) ( $val['value'] ?? '' ) ); $hit = false; if ( '' !== $attr && function_exists( 'wc_get_product' ) ) { foreach ( array_keys( (array) $ctx['variation_ids'] ) as $vid ) { $vp = wc_get_product( (int) $vid ); if ( ! $vp ) { continue; } foreach ( (array) $vp->get_attributes() as $k => $v ) { $k = strtolower( (string) $k ); if ( ( $k === $attr || $k === 'pa_' . $attr || 'pa_' . $k === $attr ) && ( '' === $want || strtolower( (string) $v ) === $want ) ) { $hit = true; break 2; } } } } return ( 'not_includes' === $op ) ? ! $hit : $hit; } private static function set_op( $have_set, $wanted_ids, $op ) { $hit = false; foreach ( $wanted_ids as $id ) { if ( isset( $have_set[ $id ] ) ) { $hit = true; break; } } return ( 'not_includes' === $op ) ? ! $hit : $hit; } private static function num_op( $a, $b, $op ) { switch ( $op ) { case '>': return $a > $b; case '<': return $a < $b; case '>=': return $a >= $b; case '<=': return $a <= $b; case '=': return abs( $a - $b ) < 0.00001; } return false; } private static function ints( $v ) { $v = is_array( $v ) ? $v : array( $v ); return array_values( array_filter( array_map( 'intval', $v ) ) ); } }