rule_id = isset( $rule_data['rule_id'] ) ? intval( $rule_data['rule_id'] ) : 0; $this->rule_name = isset( $rule_data['rule_name'] ) ? sanitize_text_field( $rule_data['rule_name'] ) : ''; $this->rule_type = isset( $rule_data['rule_type'] ) ? sanitize_text_field( $rule_data['rule_type'] ) : ''; $this->priority = isset( $rule_data['priority'] ) ? intval( $rule_data['priority'] ) : 10; $this->status = isset( $rule_data['rule_status'] ) ? sanitize_text_field( $rule_data['rule_status'] ) : 'active'; $this->start_date = isset( $rule_data['start_date'] ) ? sanitize_text_field( $rule_data['start_date'] ) : ''; $this->end_date = isset( $rule_data['end_date'] ) ? sanitize_text_field( $rule_data['end_date'] ) : ''; // Decode JSON fields - handle both string and array formats. if ( isset( $rule_data['conditions'] ) ) { if ( is_array( $rule_data['conditions'] ) ) { $this->conditions = $rule_data['conditions']; } else { $this->conditions = json_decode( $rule_data['conditions'], true ); if ( null === $this->conditions ) { $this->conditions = array(); } } } else { $this->conditions = array(); } if ( isset( $rule_data['adjustments'] ) ) { if ( is_array( $rule_data['adjustments'] ) ) { $this->adjustments = $rule_data['adjustments']; } else { $this->adjustments = json_decode( $rule_data['adjustments'], true ); if ( null === $this->adjustments ) { $this->adjustments = array(); } } } else { $this->adjustments = array(); } } } /** * Validate rule configuration. * * @return bool|WP_Error */ public function validate() { if ( empty( $this->rule_name ) ) { return new WP_Error( 'invalid_rule', __( 'Rule name is required', 'aicoso-dynamic-pricing-for-woocommerce' ) ); } if ( empty( $this->rule_type ) ) { return new WP_Error( 'invalid_rule', __( 'Rule type is required', 'aicoso-dynamic-pricing-for-woocommerce' ) ); } if ( empty( $this->adjustments ) ) { return new WP_Error( 'invalid_rule', __( 'At least one price adjustment is required', 'aicoso-dynamic-pricing-for-woocommerce' ) ); } return true; } /** * Get rule priority. * * @return int */ public function get_priority() { return $this->priority; } /** * Get rule ID. * * @return int */ public function get_id() { return $this->rule_id; } /** * Get rule name. * * @return string */ public function get_name() { return $this->rule_name; } /** * Check if should stop processing after this rule. * * @return bool */ public function stop_processing() { return $this->stop_processing; } /** * Check if rule should be disabled based on context. * * @param array $context Context data. * @return bool True if rule should be disabled, false otherwise. */ public function should_disable_rule( $context ) { // Check if rule should be disabled when products are on sale. if ( isset( $this->adjustments['disable_on_sale'] ) && '1' === $this->adjustments['disable_on_sale'] ) { if ( isset( $context['is_on_sale'] ) && $context['is_on_sale'] ) { return true; } } // Check if rule should be disabled when coupons are applied. if ( isset( $this->adjustments['disable_with_coupons'] ) && '1' === $this->adjustments['disable_with_coupons'] ) { if ( isset( $context['has_coupons'] ) && $context['has_coupons'] ) { return true; } } return false; } /** * Debug logging helper. * * @param string $message Log message. * @param array $data Additional data to log. */ protected function debug_log( $message, $data = array() ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { $log_message = '[DP_AICOSO_' . $this->rule_type . '_Rule] ' . $message; if ( ! empty( $data ) ) { $log_message .= ' | Data: ' . wp_json_encode( $data ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled error_log( $log_message ); } } /** * Check if all conditions are met. * * @param array $context Context data. * @return bool */ protected function check_conditions( $context ) { if ( empty( $this->conditions ) ) { return true; } // Debug log the conditions being checked. $this->debug_log( 'Checking conditions', array( 'conditions' => $this->conditions, 'context' => $context, ) ); foreach ( $this->conditions as $condition ) { // Skip empty or invalid conditions. if ( empty( $condition ) || ! is_array( $condition ) ) { continue; } // Skip conditions without type. if ( ! isset( $condition['type'] ) || empty( $condition['type'] ) ) { continue; } $result = $this->evaluate_condition( $condition, $context ); $this->debug_log( 'Condition evaluation result', array( 'condition' => $condition, 'result' => $result, ) ); if ( ! $result ) { $this->debug_log( 'Condition failed, rule not applicable', $condition ); return false; } } $this->debug_log( 'All conditions passed' ); return true; } /** * Evaluate a single condition. * * @param array $condition Condition data. * @param array $context Context data. * @return bool */ protected function evaluate_condition( $condition, $context ) { $type = isset( $condition['type'] ) ? $condition['type'] : ''; $operator = isset( $condition['operator'] ) ? $condition['operator'] : '='; $value = isset( $condition['value'] ) ? $condition['value'] : ''; // If value is empty and it's not a presence check, skip the condition. if ( '' === $value && ! in_array( $operator, array( 'exists', 'not_exists' ), true ) ) { $this->debug_log( 'Skipping condition with empty value', $condition ); return true; } switch ( $type ) { case 'user_role': return $this->check_user_role_condition( $context, $operator, $value ); case 'quantity': return $this->check_quantity_condition( $context, $operator, $value ); case 'cart_total': return $this->check_cart_total_condition( $context, $operator, $value ); case 'date': return $this->check_date_condition( $context, $operator, $value ); default: /** * Filter to evaluate custom conditions. * * @since 1.0.0 * * @param bool $result The condition result. * @param array $condition The condition data. * @param array $context The evaluation context. */ return apply_filters( 'dp_aicoso_evaluate_condition', true, $condition, $context ); } } /** * Check user role condition. * * @param array $context Context data. * @param string $operator Comparison operator. * @param mixed $value Value to compare. * @return bool */ protected function check_user_role_condition( $context, $operator, $value ) { $user_role = isset( $context['user_role'] ) ? $context['user_role'] : 'guest'; // Handle comma-separated roles if provided as string. if ( is_string( $value ) && strpos( $value, ',' ) !== false ) { $value = array_map( 'trim', explode( ',', $value ) ); } $this->debug_log( 'Checking user role condition', array( 'user_role' => $user_role, 'operator' => $operator, 'value' => $value, ) ); switch ( $operator ) { case '=': case 'equals': if ( is_array( $value ) ) { return in_array( $user_role, $value, true ); } return $user_role === $value; case '!=': case 'not_equals': if ( is_array( $value ) ) { return ! in_array( $user_role, $value, true ); } return $user_role !== $value; case 'in': return in_array( $user_role, (array) $value, true ); case 'not_in': return ! in_array( $user_role, (array) $value, true ); default: $this->debug_log( 'Unknown operator for user role condition: ' . $operator ); return false; } } /** * Check quantity condition. * * @param array $context Context data. * @param string $operator Comparison operator. * @param mixed $value Value to compare. * @return bool */ protected function check_quantity_condition( $context, $operator, $value ) { // First check if cart_quantity is available (for cart-wide quantity checks). // Otherwise fall back to individual product quantity. if ( isset( $context['cart_quantity'] ) ) { $quantity = intval( $context['cart_quantity'] ); } else { $quantity = isset( $context['quantity'] ) ? intval( $context['quantity'] ) : 1; } $value = intval( $value ); $this->debug_log( 'Checking quantity condition', array( 'quantity' => $quantity, 'cart_quantity' => isset( $context['cart_quantity'] ) ? $context['cart_quantity'] : 'not set', 'operator' => $operator, 'value' => $value, ) ); switch ( $operator ) { case '=': case 'equals': return $quantity === $value; case '!=': case 'not_equals': return $quantity !== $value; case '>': case 'greater_than': return $quantity > $value; case '>=': case 'greater_than_or_equal': return $quantity >= $value; case '<': case 'less_than': return $quantity < $value; case '<=': case 'less_than_or_equal': return $quantity <= $value; default: $this->debug_log( 'Unknown operator for quantity condition: ' . $operator ); return false; } } /** * Check cart total condition. * * @param array $context Context data. * @param string $operator Comparison operator. * @param mixed $value Value to compare. * @return bool */ protected function check_cart_total_condition( $context, $operator, $value ) { $cart_total = isset( $context['cart_total'] ) ? floatval( $context['cart_total'] ) : 0; // Also check cart_subtotal if cart_total is 0. if ( 0 === $cart_total && isset( $context['cart_subtotal'] ) ) { $cart_total = floatval( $context['cart_subtotal'] ); } $value = floatval( $value ); $this->debug_log( 'Checking cart total condition', array( 'cart_total' => $cart_total, 'operator' => $operator, 'value' => $value, ) ); switch ( $operator ) { case '=': case 'equals': return abs( $cart_total - $value ) < 0.01; // Use epsilon for float comparison. case '!=': case 'not_equals': return abs( $cart_total - $value ) >= 0.01; case '>': case 'greater_than': return $cart_total > $value; case '>=': case 'greater_than_or_equal': return $cart_total >= $value; case '<': case 'less_than': return $cart_total < $value; case '<=': case 'less_than_or_equal': return $cart_total <= $value; default: $this->debug_log( 'Unknown operator for cart total condition: ' . $operator ); return false; } } /** * Check date condition. * * @param array $context Context data. * @param string $operator Comparison operator. * @param mixed $value Value to compare. * @return bool */ protected function check_date_condition( $context, $operator, $value ) { $current_time = isset( $context['timestamp'] ) ? $context['timestamp'] : current_time( 'timestamp' ); $compare_time = strtotime( $value ); if ( ! $compare_time ) { return false; } switch ( $operator ) { case 'after': return $current_time > $compare_time; case 'before': return $current_time < $compare_time; case 'between': if ( is_array( $value ) && count( $value ) === 2 ) { $start = strtotime( $value[0] ); $end = strtotime( $value[1] ); return $current_time >= $start && $current_time <= $end; } return false; default: return false; } } /** * Apply price adjustment. * * @param float $price Original price. * @param array $adjustment Adjustment configuration. * @return float Modified price. */ protected function apply_adjustment( $price, $adjustment ) { $type = isset( $adjustment['type'] ) ? $adjustment['type'] : 'fixed'; $value = isset( $adjustment['value'] ) ? floatval( $adjustment['value'] ) : 0; switch ( $type ) { case 'percentage': // Percentage discount. $discount = $price * ( $value / 100 ); return max( 0, $price - $discount ); case 'fixed': // Fixed amount discount. return max( 0, $price - $value ); case 'fixed_price': // Set to fixed price. return max( 0, $value ); default: /** * Filter the adjusted price. * * @since 1.0.0 * * @param float $price The original price. * @param array $adjustment The adjustment settings. * @param string $type The adjustment type. * @param float $value The adjustment value. */ return apply_filters( 'dp_aicoso_apply_adjustment', $price, $adjustment, $type, $value ); } } /** * Check if rule is active within date range. * * @return bool */ protected function is_within_date_range() { $current_time = current_time( 'timestamp' ); // Check start date. if ( ! empty( $this->start_date ) ) { $start_time = strtotime( $this->start_date ); if ( $start_time && $current_time < $start_time ) { return false; } } // Check end date. if ( ! empty( $this->end_date ) ) { $end_time = strtotime( $this->end_date ); if ( $end_time && $current_time > $end_time ) { return false; } } return true; } /** * Save rule to database. * * @return int|WP_Error Rule ID on success, WP_Error on failure. */ public function save() { global $wpdb; // Validate rule. $valid = $this->validate(); if ( is_wp_error( $valid ) ) { return $valid; } $table_name = $wpdb->prefix . 'dp_aicoso_rules'; // Prepare data for database operations. $data = array( 'rule_name' => $this->rule_name, 'rule_type' => $this->rule_type, 'rule_status' => $this->status, 'priority' => $this->priority, 'conditions' => wp_json_encode( $this->conditions ), 'adjustments' => wp_json_encode( $this->adjustments ), 'start_date' => $this->start_date, 'end_date' => $this->end_date, ); // Format placeholders for data types. $format = array( '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' ); if ( $this->rule_id > 0 ) { // Update existing rule - $wpdb->update() properly escapes with format parameters. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->update( $table_name, $data, array( 'rule_id' => $this->rule_id ), $format, array( '%d' ) ); if ( false === $result ) { return new WP_Error( 'db_error', __( 'Failed to update rule', 'aicoso-dynamic-pricing-for-woocommerce' ) ); } // Clear cache for the updated rule. wp_cache_delete( 'dp_aicoso_rule_' . $this->rule_id, 'dp_aicoso_rules' ); wp_cache_delete( 'dp_aicoso_rules_' . $this->rule_type, 'dp_aicoso_rules' ); } else { // Insert new rule - $wpdb->insert() properly escapes with format parameters. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->insert( $table_name, $data, $format ); if ( false === $result ) { return new WP_Error( 'db_error', __( 'Failed to create rule', 'aicoso-dynamic-pricing-for-woocommerce' ) ); } $this->rule_id = $wpdb->insert_id; // Clear cache for the new rule type. wp_cache_delete( 'dp_aicoso_rules_' . $this->rule_type, 'dp_aicoso_rules' ); } // Clear cache. $this->clear_cache(); return $this->rule_id; } /** * Clear related cache. */ protected function clear_cache() { // Clear price cache. delete_transient( 'dp_aicoso_rules_' . $this->rule_type ); // Clear object cache with consistent cache group. wp_cache_delete( 'dp_aicoso_rule_' . $this->rule_id, 'dp_aicoso_rules' ); wp_cache_delete( 'dp_aicoso_rules_' . $this->rule_type, 'dp_aicoso_rules' ); /** * Action fired when rule cache is cleared. * * @since 1.0.0 * * @param int $rule_id The rule ID. * @param string $rule_type The rule type. */ do_action( 'dp_aicoso_rule_cache_cleared', $this->rule_id, $this->rule_type ); } }