init(); } /** * Initialize the cart handler. */ private function init() { // Ensure WooCommerce is available. if ( ! class_exists( 'WooCommerce' ) ) { return; } // Get calculator instance. if ( class_exists( 'DP_AICOSO_Price_Calculator' ) ) { $this->calculator = DP_AICOSO_Price_Calculator::get_instance(); } // Hook into cart with very high priority to run last. add_action( 'woocommerce_before_calculate_totals', array( $this, 'apply_dynamic_pricing' ), 9999 ); // Hook to store original prices when item is added to cart. add_filter( 'woocommerce_add_cart_item', array( $this, 'store_original_price' ), 10, 2 ); // Hook to restore data from session. add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'restore_original_price' ), 10, 2 ); // Clear stored prices when cart is emptied. add_action( 'woocommerce_cart_emptied', array( $this, 'clear_stored_prices' ) ); // Update prices when quantity changes via AJAX. add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'quantity_changed' ), 10, 4 ); } /** * Apply dynamic pricing to cart items. * This is the MAIN method that handles all cart pricing. * * @param WC_Cart $cart Cart object. */ public function apply_dynamic_pricing( $cart ) { // Safety checks. if ( ! $this->should_process_cart( $cart ) ) { return; } // Set processing flag to prevent loops. $this->processing = true; // Process each cart item. foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { $this->process_cart_item( $cart_item_key, $cart_item ); } // Reset processing flag. $this->processing = false; } /** * Check if we should process the cart. * * @param WC_Cart $cart Cart object. * @return bool */ private function should_process_cart( $cart ) { // Don't process if already processing (prevent loops). if ( $this->processing ) { return false; } // Don't process empty cart. if ( $cart->is_empty() ) { return false; } // Don't process in admin unless doing AJAX. if ( is_admin() && ! wp_doing_ajax() ) { return false; } // Check calculator is available. if ( ! $this->calculator ) { return false; } return true; } /** * Process a single cart item. * * @param string $cart_item_key Cart item key. * @param array $cart_item Cart item data. */ private function process_cart_item( $cart_item_key, &$cart_item ) { // Get the product object from cart. $product = $cart_item['data']; if ( ! $product ) { return; } // Get quantity. $quantity = isset( $cart_item['quantity'] ) ? intval( $cart_item['quantity'] ) : 1; // Get the original price (stored when added to cart). $original_price = $this->get_original_price( $cart_item_key, $cart_item, $product ); if ( $original_price <= 0 ) { return; } // Calculate dynamic price using our pricing rules. $dynamic_price = $this->calculate_dynamic_price( $product, $quantity, $original_price ); // Apply the calculated price. $this->apply_price_to_product( $product, $dynamic_price, $original_price, $cart_item_key ); // Store pricing info in cart item for reference. WC()->cart->cart_contents[ $cart_item_key ]['dp_aicoso_pricing'] = array( 'original_price' => $original_price, 'dynamic_price' => $dynamic_price, 'quantity' => $quantity, 'discount_applied' => ( $dynamic_price < $original_price ), 'discount_amount' => $original_price - $dynamic_price, 'discount_percent' => ( $original_price > 0 ) ? round( ( ( $original_price - $dynamic_price ) / $original_price ) * 100, 2 ) : 0, ); } /** * Get the original price for a cart item. * * @param string $cart_item_key Cart item key. * @param array $cart_item Cart item data. * @param WC_Product $product Product object. * @return float Original price. */ private function get_original_price( $cart_item_key, $cart_item, $product ) { // First check if we have it stored. if ( isset( $cart_item['dp_aicoso_original_price'] ) ) { return floatval( $cart_item['dp_aicoso_original_price'] ); } // Check our class storage. if ( isset( $this->original_prices[ $cart_item_key ] ) ) { return floatval( $this->original_prices[ $cart_item_key ] ); } // Get from product (use 'edit' context to get raw price). $price = floatval( $product->get_regular_price( 'edit' ) ); // Check for sale price. $sale_price = $product->get_sale_price( 'edit' ); if ( $sale_price && $sale_price > 0 ) { $price = floatval( $sale_price ); } // Store for future use. $this->original_prices[ $cart_item_key ] = $price; return $price; } /** * Calculate dynamic price for a product. * * @param WC_Product $product Product object. * @param int $quantity Quantity. * @param float $original_price Original price. * @return float Dynamic price. */ private function calculate_dynamic_price( $product, $quantity, $original_price ) { // Check if we should apply discounts based on quantity limits. if ( ! $this->should_apply_discount_for_quantity( $product, $quantity ) ) { return $original_price; } // Use the calculator to get dynamic price. $dynamic_price = $this->calculator->calculate_product_price( $product, $quantity ); // If calculator returns the same as original, no discount should apply. if ( abs( $dynamic_price - $original_price ) < 0.01 ) { return $original_price; } // Validate the price. if ( $dynamic_price <= 0 ) { return $original_price; } // Only apply discount if it's actually less than original. if ( $dynamic_price >= $original_price ) { return $original_price; } // Ensure we don't go below zero. return max( 0, $dynamic_price ); } /** * Check if discount should apply for given quantity. * This is a fail-safe to ensure we respect max quantity limits. * * @param WC_Product $product Product object. * @param int $quantity Quantity. * @return bool */ private function should_apply_discount_for_quantity( $product, $quantity ) { // Get all active rules. global $wpdb; $table_name = $wpdb->prefix . 'dp_aicoso_rules'; // Check if table exists. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check, cached in parent method $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name; if ( ! $table_exists ) { return true; // No rules table, allow calculator to decide. } // Get active product rules. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Rule validation check, performance-critical path $rules = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}dp_aicoso_rules WHERE rule_status = %s AND rule_type IN ('product', 'category', 'custom') ORDER BY priority DESC", 'active' ), ARRAY_A ); if ( empty( $rules ) ) { return true; // No rules, allow calculator to decide. } $product_id = $product->get_id(); $product_categories = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) ); // Check each rule for quantity limits. foreach ( $rules as $rule_data ) { // Check if this rule applies to this product. $rule_applies = false; if ( 'product' === $rule_data['rule_type'] ) { // Check if rule applies to this product. $adjustments = json_decode( $rule_data['adjustments'], true ); // Check if it applies to all products. if ( isset( $adjustments['apply_to_all'] ) && $adjustments['apply_to_all'] ) { $rule_applies = true; } elseif ( isset( $adjustments['product_ids'] ) && is_array( $adjustments['product_ids'] ) ) { // Check specific product IDs. if ( in_array( $product_id, $adjustments['product_ids'], true ) ) { $rule_applies = true; } } } elseif ( 'category' === $rule_data['rule_type'] ) { // Check if rule applies to product's categories. $adjustments = json_decode( $rule_data['adjustments'], true ); if ( isset( $adjustments['category_ids'] ) && is_array( $adjustments['category_ids'] ) ) { foreach ( $product_categories as $cat_id ) { if ( in_array( $cat_id, $adjustments['category_ids'], true ) ) { $rule_applies = true; break; } } } } // If rule doesn't apply to this product, skip it. if ( ! $rule_applies ) { continue; } $adjustments = json_decode( $rule_data['adjustments'], true ); // Check if rule has quantity ranges. if ( isset( $adjustments['quantity_ranges'] ) && ! empty( $adjustments['quantity_ranges'] ) ) { // Find the maximum quantity across all ranges. $max_quantity_in_ranges = 0; $has_unlimited = false; foreach ( $adjustments['quantity_ranges'] as $range ) { $max_qty = isset( $range['max_quantity'] ) ? $range['max_quantity'] : null; // Check for unlimited range. if ( null === $max_qty || '' === $max_qty || '0' === $max_qty || 0 === $max_qty ) { $has_unlimited = true; break; } // Track the highest max quantity. $max_qty_int = intval( $max_qty ); if ( $max_qty_int > $max_quantity_in_ranges ) { $max_quantity_in_ranges = $max_qty_int; } } // If no unlimited range and quantity exceeds all maximums, no discount. if ( ! $has_unlimited && $max_quantity_in_ranges > 0 && $quantity > $max_quantity_in_ranges ) { return false; } } } return true; // Allow discount. } /** * Apply the calculated price to the product in cart. * * @param WC_Product $product Product object. * @param float $dynamic_price Dynamic price to apply. * @param float $original_price Original price for reference. * @param string $cart_item_key Cart item key. */ private function apply_price_to_product( $product, $dynamic_price, $original_price, $cart_item_key ) { // Set the price on the product object. $product->set_price( $dynamic_price ); } /** * Store original price when item is added to cart. * * @param array $cart_item Cart item data. * @param string $cart_item_key Cart item key. * @return array Modified cart item. */ public function store_original_price( $cart_item, $cart_item_key ) { if ( isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ) { $product = $cart_item['data']; // Get the original price. $original_price = floatval( $product->get_regular_price( 'edit' ) ); // Check for sale price. $sale_price = $product->get_sale_price( 'edit' ); if ( $sale_price && $sale_price > 0 ) { $original_price = floatval( $sale_price ); } // Store in cart item. $cart_item['dp_aicoso_original_price'] = $original_price; // Also store in our class property. $this->original_prices[ $cart_item_key ] = $original_price; } return $cart_item; } /** * Restore original price from session. * * @param array $cart_item Cart item data. * @param array $values Session values. * @return array Modified cart item. */ public function restore_original_price( $cart_item, $values ) { if ( isset( $values['dp_aicoso_original_price'] ) ) { $cart_item['dp_aicoso_original_price'] = $values['dp_aicoso_original_price']; } if ( isset( $values['dp_aicoso_pricing'] ) ) { $cart_item['dp_aicoso_pricing'] = $values['dp_aicoso_pricing']; } return $cart_item; } /** * Clear stored prices when cart is emptied. */ public function clear_stored_prices() { $this->original_prices = array(); } /** * Handle quantity changes. * * @param string $cart_item_key Cart item key. * @param int $quantity New quantity. * @param int $old_quantity Old quantity. * @param WC_Cart $cart Cart object. */ public function quantity_changed( $cart_item_key, $quantity, $old_quantity, $cart ) { // Trigger recalculation when quantity changes. $cart->calculate_totals(); } /** * Log messages for debugging. * * @param string $message Message to log. */ private function log( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled error_log( 'DP_AICOSO Cart Handler: ' . $message ); } } } // Initialize the cart handler. add_action( 'init', function () { if ( class_exists( 'WooCommerce' ) ) { DP_AICOSO_Cart_Handler::get_instance(); } } );